web-dev-qa-db-fra.com

Est-il judicieux de créer des blocs juste pour réduire la portée d'une variable?

J'écris un programme dans Java où à un moment donné, je dois charger un mot de passe pour mon magasin de clés. Juste pour le plaisir, j'ai essayé de garder mon mot de passe dans Java aussi court que possible en faisant ceci:

//Some code
....

KeyManagerFactory keyManager = KeyManagerFactory.getInstance("SunX509");
Keystore keyStore = KeyStore.getInstance("JKS");

{
    char[] password = getPassword();
    keyStore.load(new FileInputStream(keyStoreLocation), password);
    keyManager.init(keyStore, password);
}

...
//Some more code

Maintenant, je sais que dans ce cas, c'est un peu stupide. Il y a un tas d'autres choses que j'aurais pu faire, la plupart en fait mieux (je n'aurais pas pu utiliser de variable du tout).

Cependant, j'étais curieux de savoir s'il y avait un cas où faire cela n'était pas si stupide. La seule autre chose à laquelle je peux penser est que si vous vouliez réutiliser des noms de variables communs comme count ou temp, mais de bonnes conventions de dénomination et des méthodes courtes rendent peu probable ce qui serait utile.

Existe-t-il un cas où l'utilisation de blocs uniquement pour réduire la portée des variables est logique?

39
Lord Farquaad

Tout d'abord, en parlant de la mécanique sous-jacente:

En C++ scope == life, les destructeurs b/c sont invoqués à la sortie du scope. De plus, une distinction importante en C/C++, nous pouvons déclarer des objets locaux. Dans le runtime pour C/C++, le compilateur alloue généralement un cadre de pile pour la méthode qui est aussi grande que nécessaire, à l'avance, plutôt que d'allouer plus d'espace de pile à l'entrée à chaque portée (qui déclare des variables). Ainsi, le compilateur réduit ou aplatit les étendues.

Le compilateur C/C++ peut réutiliser l'espace de stockage de pile pour les sections locales qui n'entrent pas en conflit dans la durée de vie (il utilisera généralement l'analyse du code réel pour le déterminer plutôt que la portée, b/c qui est plus précis que la portée!).

Je mentionne la syntaxe Java C/C++ b/c, c'est-à-dire les accolades et la portée, dérivée au moins en partie de cette famille. Et aussi parce que C++ est apparu dans les commentaires des questions.

En revanche, il n'est pas possible d'avoir des objets locaux en Java: tous les objets sont des objets tas, et tous les locaux/formels sont soit des variables de référence soit des types primitifs (btw, il en va de même pour la statique).

De plus, dans Java portée et durée de vie ne sont pas exactement égales: être dans/hors de portée est principalement un concept de temps de compilation allant vers l'accessibilité et les conflits de noms; rien ne se passe vraiment dans Java à la sortie de la portée en ce qui concerne le nettoyage des variables. Le garbage collection de Java détermine (le point final de la) durée de vie des objets.

Le mécanisme de bytecode de Java (la sortie du compilateur Java) a tendance à promouvoir les variables utilisateur déclarées dans des étendues limitées au niveau supérieur de la méthode, car il n'y a pas de fonctionnalité d'étendue au niveau du bytecode (ce est similaire à la gestion C/C++ de la pile.) Au mieux, le compilateur pourrait réutiliser les emplacements de variables locaux, mais (contrairement à C/C++) uniquement si leur type est le même.

(Cependant, pour être sûr que le compilateur JIT sous-jacent dans le runtime pourrait réutiliser le même espace de stockage de pile pour deux sections locales de type différent (et différent fendu), avec une analyse suffisante du bytecode.)

S'agissant de l'avantage du programmeur, j'aurais tendance à être d'accord avec d'autres pour dire qu'une méthode (privée) est une meilleure construction même si elle n'est utilisée qu'une seule fois.

Pourtant, il n'y a rien de vraiment mal à l'utiliser pour déconflire les noms.

Je l'ai fait dans de rares circonstances lorsque l'élaboration de méthodes individuelles est un fardeau. Par exemple, lors de l'écriture d'un interpréteur à l'aide d'une grande instruction switch dans une boucle, je pourrais (selon les facteurs) introduire un bloc distinct pour chaque case afin de garder les cas individuels plus séparés les uns des autres , au lieu de faire de chaque cas une méthode différente (dont chacune n'est invoquée qu'une seule fois).

(Notez qu'en tant que code en blocs, les cas individuels ont accès aux instructions "break;" et "continue;" concernant la boucle englobante, alors que les méthodes nécessiteraient de renvoyer des booléens et l'utilisation de conditions par l'appelant pour accéder à ces flux de contrôle déclarations.)

35
Erik Eidt

À mon avis, il serait plus clair de retirer le bloc selon sa propre méthode. Si vous avez laissé ce bloc, j'espère voir un commentaire clarifiant pourquoi vous y mettez le bloc en premier lieu. À ce stade, il semble plus propre d'avoir l'appel de méthode à initializeKeyManager() qui maintient la variable de mot de passe limitée à la portée de la méthode et montre votre intention avec le bloc de code.

27
Casey Langford

Ils peuvent être utiles à Rust, avec ses règles d'emprunt strictes. Et généralement, dans les langages fonctionnels, où ce bloc renvoie une valeur. Mais dans des langages comme Java? Bien que l'idée semble élégante, en pratique, elle est rarement utilisée, de sorte que la confusion de la voir éclipsera la clarté potentielle de la limitation de la portée des variables.

Il y a cependant un cas où je trouve cela utile - dans une instruction switch! Parfois, vous voulez déclarer une variable dans un case:

switch (op) {
    case ADD:
        int result = a + b;
        System.out.printf("%s + %s = %s\n", a, b, result);
        break;
    case SUB:
        int result = a - b;
        System.out.printf("%s - %s = %s\n", a, b, result);
        break;
}

Cependant, cela échouera:

error: variable result is already defined in method main(String[])
            int result = a - b;

switch les instructions sont étranges - ce sont des instructions de contrôle, mais en raison de leur échec, elles n'introduisent pas d'étendues.

Donc, vous pouvez simplement utiliser des blocs pour créer vous-même les étendues:

switch (op) {
    case ADD: {
        int result = a + b;
        System.out.printf("%s + %s = %s\n", a, b, result);
    } break;
    case SUB: {
        int result = a - b;
        System.out.printf("%s - %s = %s\n", a, b, result);
    } break;
}
17
Idan Arye
  • Cas général:

Existe-t-il un cas où l'utilisation de blocs uniquement pour réduire la portée des variables est logique?

Il est généralement considéré comme une bonne pratique de garder les méthodes courtes. Réduire la portée de certaines variables peut être un signe que votre méthode est assez longue, suffisamment pour que vous pensiez que la portée des variables doit être réduite. Dans ce cas, cela vaut probablement la peine de créer une méthode distincte pour cette partie du code.

Les cas que je trouverais problématiques, c'est lorsque cette partie du code utilise plus d'une poignée d'arguments. Dans certaines situations, vous pourriez vous retrouver avec de petites méthodes qui prennent chacune beaucoup de paramètres (ou nécessiter une solution de contournement pour simuler le retour de plusieurs valeurs). La solution à ce problème particulier est de créer une autre classe qui conserve les différentes variables que vous utilisez et implémente implémente toutes les étapes que vous avez en tête dans ses méthodes relativement courtes: c'est un cas d'utilisation typique pour utiliser un Builder modèle.

Cela dit, toutes ces directives de codage peuvent parfois être appliquées de manière lâche. Il existe parfois des cas étranges, où le code peut être plus lisible si vous le conservez dans une méthode légèrement plus longue, au lieu de le diviser en petites sous-méthodes (ou modèles de générateur). En règle générale, cela fonctionne pour les problèmes de taille moyenne, assez linéaires et où trop de fractionnement entrave réellement la lisibilité (surtout si vous avez besoin de sauter entre trop de méthodes pour comprendre ce que fait le code).

Cela peut avoir du sens, mais c'est très rare.

  • Votre cas d'utilisation:

Voici votre code:

KeyManagerFactory keyManager = KeyManagerFactory.getInstance("SunX509");
Keystore keyStore = KeyStore.getInstance("JKS");

{
    char[] password = getPassword();
    keyStore.load(new FileInputStream(keyStoreLocation), password);
    keyManager.init(keyStore, password);
}

Vous créez un FileInputStream, mais vous ne le fermez jamais.

Une façon de résoudre ce problème est d'utiliser une instruction try-with-resources . Il étend le InputStream, et vous pouvez également utiliser ce bloc pour réduire la portée d'autres variables si vous le souhaitez (dans des limites raisonnables, car ces lignes sont liées):

KeyManagerFactory keyManager = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
Keystore keyStore = KeyStore.getInstance("JKS");

try (InputStream fis = new FileInputStream(keyStoreLocation)) {
    char[] password = getPassword();
    keyStore.load(fis, password);
    keyManager.init(keyStore, password);
}

(Il est aussi souvent préférable d'utiliser getDefaultAlgorithm() au lieu de SunX509, Soit dit en passant.)

En outre, alors que les conseils pour séparer des morceaux de code en méthodes distinctes sont généralement judicieux. Cela vaut rarement la peine si ce n'est que pour 3 lignes (si c'est le cas, la création d'une méthode distincte entrave la lisibilité).

6
Bruno

À mon humble avis, c'est généralement quelque chose à éviter en grande partie pour le code de production, car vous n'êtes généralement pas tenté de le faire, sauf dans les fonctions sporadiques qui effectuent des tâches disparates. J'ai tendance à le faire dans un code de rebut utilisé pour tester les choses, mais je ne trouve aucune tentation de le faire dans le code de production où j'ai réfléchi à l'avance à ce que chaque fonction devrait faire, car alors la fonction aura naturellement un très portée limitée par rapport à son état local.

Je n'ai jamais vraiment vu d'exemples de blocs anonymes utilisés comme ceci (n'impliquant pas de conditionnels, un bloc try pour une transaction, etc.) pour réduire la portée de manière significative dans une fonction qui n'a pas posé la question pourquoi il ne pouvait pas être divisé en fonctions plus simples avec des portées réduites s'il bénéficiait réellement d'un point de vue SE authentique des blocs anonymes. C'est généralement du code éclectique qui fait un tas de choses vaguement liées ou très indépendantes où nous sommes le plus tentés d'atteindre cela.

Par exemple, si vous essayez de le faire pour réutiliser une variable nommée count, cela suggère que vous comptez deux choses disparates. Si le nom de la variable va être aussi court que count, alors il est logique pour moi de le lier au contexte de la fonction, qui pourrait potentiellement simplement compter un type de chose. Ensuite, vous pouvez instantanément consulter le nom et/ou la documentation de la fonction, voir count, et savoir instantanément ce que cela signifie dans le contexte de ce que fait la fonction sans analyser tout le code. Je ne trouve pas souvent un bon argument pour qu'une fonction compte deux choses différentes en réutilisant le même nom de variable de manière à rendre les étendues/blocs anonymes si attrayants par rapport aux alternatives. Cela ne veut pas dire que toutes les fonctions ne doivent compter qu'une seule chose. Je dis que je vois peu d'avantages d'ingénierie pratiques pour une fonction réutilisant le même nom de variable pour compter deux ou plusieurs choses et utilisant des blocs anonymes pour limiter la portée de chaque comptage individuel. Si la fonction est simple et claire, ce n'est pas la fin du monde d'avoir deux variables de comptage nommées différemment, la première ayant peut-être quelques lignes de visibilité de portée de plus que ce qui est idéalement requis. De telles fonctions ne sont généralement pas la source d'erreurs dépourvues de tels blocs anonymes pour réduire encore plus l'étendue minimale de ses variables locales.

Pas une suggestion pour les méthodes superflues

Il ne s'agit pas de vous suggérer de créer des méthodes avec force, soit simplement pour réduire la portée. C'est sans doute tout aussi mauvais ou pire, et ce que je suggère ne devrait pas appeler un besoin de méthodes "d'assistance" privées maladroites plus qu'un besoin de portées anonymes. C'est trop penser au code tel qu'il est maintenant et comment réduire la portée des variables que penser à la façon de résoudre conceptuellement le problème au niveau de l'interface de manière à fournir une visibilité propre et courte des états de fonction locaux naturellement sans imbrication profonde des blocs et 6+ niveaux d'indentation. Je suis d'accord avec Bruno que vous pouvez entraver la lisibilité du code en insérant de force 3 lignes de code dans une fonction, mais cela commence avec l'hypothèse que vous faites évoluer les fonctions que vous créez en fonction de votre implémentation existante, plutôt que de concevoir les fonctions sans vous embrouiller dans les implémentations. Si vous le faites de cette façon, j'ai trouvé peu de besoin de blocs anonymes qui ne servent à rien au-delà de la réduction de la portée des variables dans une méthode donnée, sauf si vous essayez avec zèle de réduire la portée d'une variable de quelques lignes de code inoffensif de moins où l'introduction exotique de ces blocs anonymes contribue sans doute autant aux frais généraux intellectuels qu'ils en suppriment.

Essayer de réduire encore plus les portées minimales

Si la réduction des étendues de variables locales au minimum absolu en valait la peine, il devrait y avoir une large acceptation du code comme ceci:

ImageIO.write(new Robot("borg").createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())), "png", new File(Db.getUserId(User.handle()).toString()));

... car cela entraîne la visibilité minimale de l'état en ne créant même pas de variables pour s'y référer en premier lieu. Je ne veux pas paraître dogmatique, mais je pense en fait que la solution pragmatique est d'éviter les blocages anonymes lorsque cela est possible, tout comme c'est d'éviter la monstrueuse ligne de code ci-dessus, et s'ils semblent si absolument nécessaires dans un contexte de production à partir de la perspective d'exactitude et de maintien des invariants dans une fonction, alors je pense vraiment que la façon dont vous organisez votre code en fonctions et la conception de vos interfaces mérite d'être réexaminée. Naturellement, si votre méthode fait 400 lignes et que la portée d'une variable est visible pour 300 lignes de code de plus que nécessaire, cela pourrait être un véritable problème d'ingénierie, mais ce n'est pas nécessairement un problème à résoudre avec des blocs anonymes.

À tout le moins, l'utilisation de blocs anonymes partout est exotique, pas idiomatique, et le code exotique risque d'être détesté par d'autres, sinon par vous-même, des années plus tard.

L'utilité pratique de réduire la portée

L'utilité ultime de réduire la portée des variables est de vous permettre d'obtenir une gestion d'état correcte et de la maintenir correcte et de vous permettre de raisonner facilement sur ce que fait une partie donnée d'une base de code - pour pouvoir maintenir des invariants conceptuels. Si la gestion de l'état local d'une seule fonction est si complexe que vous devez réduire de manière forcée la portée avec un bloc anonyme dans le code qui n'est pas censé être finalisé et bon, là encore, c'est un signe pour moi que la fonction elle-même doit être réexaminée . Si vous avez des difficultés à raisonner sur la gestion d'état des variables dans une portée de fonction locale, imaginez la difficulté de raisonner sur les variables privées accessibles à toutes les méthodes d'une classe entière. Nous ne pouvons pas utiliser de blocs anonymes pour réduire leur visibilité. Pour moi, il est utile de commencer par accepter que les variables auront tendance à avoir une portée légèrement plus large que ce qu'elles devraient idéalement avoir dans de nombreuses langues, à condition que cela ne devienne pas incontrôlable au point où vous aurez du mal à maintenir les invariants. Ce n'est pas quelque chose à résoudre autant avec des blocs anonymes que je le vois comme accepté d'un point de vue pragmatique.

2
user204677

Existe-t-il un cas où l'utilisation de blocs uniquement pour réduire la portée des variables est logique?

Je pense que la motivation est une raison suffisante pour introduire un blocage, oui.

Comme l'a noté Casey, le bloc est une "odeur de code" qui indique que vous feriez mieux d'extraire le bloc dans une fonction. Mais vous ne pouvez pas.

John Carmark a écrit un mémo sur le code intégré en 2007. Son résumé

  • Si une fonction n'est appelée qu'à partir d'un seul endroit, envisagez de l'inclure.
  • Si une fonction est appelée à partir de plusieurs endroits, voyez s'il est possible d'organiser le travail à un seul endroit, peut-être avec des indicateurs, et insérez-le.
  • S'il existe plusieurs versions d'une fonction, envisagez de créer une seule fonction avec plus de paramètres, éventuellement par défaut.
  • Si le travail est presque purement fonctionnel, avec peu de références à l'état global, essayez de le rendre complètement fonctionnel.

Alors pensez , faites un choix avec un but et (facultatif) laissez des preuves décrivant votre motivation pour le choix que vous avez fait.

2
VoiceOfUnreason

Mon opinion peut être controversée, mais oui, je pense qu'il y a certainement des situations où j'utiliserais ce genre de style. Cependant, "juste pour réduire la portée de la variable" n'est pas un argument valide, car c'est ce que vous faites déjà. Et faire quelque chose juste pour le faire n'est pas une raison valable. Notez également que cette explication n'a aucun sens si votre équipe a déjà déterminé si ce type de syntaxe est conventionnelle ou non.

Je suis principalement un programmeur C #, mais j'ai entendu cela en Java, renvoyant un mot de passe comme char[] est pour vous permettre de réduire la durée de conservation du mot de passe en mémoire. Dans cet exemple, cela n'aidera pas, car le garbage collector est autorisé à collecter le tableau à partir du moment où il n'est pas utilisé, donc peu importe si le mot de passe quitte la portée. Je ne me demande pas s'il est viable d'effacer le tableau une fois que vous en avez terminé, mais dans ce cas, si vous voulez le faire, la portée de la variable est logique:

{
    char[] password = getPassword();
    try{
        keyStore.load(new FileInputStream(keyStoreLocation), password);
        keyManager.init(keyStore, password);
    }finally{
        Arrays.fill(password, '\0');
    }
}

Ceci est vraiment similaire à l'instruction try-with-resources, car elle étend la ressource et effectue une finalisation dessus une fois que vous avez terminé. Encore une fois, veuillez noter que je ne préconise pas de gérer les mots de passe de cette manière, mais que si vous décidez de le faire, ce style est raisonnable.

La raison en est que la variable n'est plus valide. Vous l'avez créé, utilisé et invalidé son état pour ne contenir aucune information significative. Cela n'a aucun sens d'utiliser la variable après ce bloc, il est donc raisonnable de l'étendre.

Un autre exemple auquel je peux penser est lorsque vous avez deux variables qui ont un nom et une signification similaires, mais vous travaillez avec l'une puis avec l'autre, et vous voulez les garder séparées. J'ai écrit ce code en C #:

{
    MethodBuilder m_ToString = tb.DefineMethod("ToString", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, typeofString, Type.EmptyTypes);
    var il = m_ToString.GetILGenerator();
    il.Emit(OpCodes.Ldstr, templateType.ToString()+":"+staticType.ToString());
    il.Emit(OpCodes.Ret);
    tb.DefineMethodOverride(m_ToString, m_Object_ToString);
}
{
    PropertyBuilder p_Class = tb.DefineProperty("Class", PropertyAttributes.None, typeofType, Type.EmptyTypes);
    MethodBuilder m_get_Class = tb.DefineMethod("get_Class", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, typeofType, Type.EmptyTypes);
    var il = m_get_Class.GetILGenerator();
    il.Emit(OpCodes.Ldtoken, staticType);
    il.Emit(OpCodes.Call, m_Type_GetTypeFromHandle);
    il.Emit(OpCodes.Ret);
    p_Class.SetGetMethod(m_get_Class);
    tb.DefineMethodOverride(m_get_Class, m_IPattern_get_Class);
}

Vous pouvez dire que je peux simplement déclarer ILGenerator il; en haut de la méthode, mais je ne veux pas non plus réutiliser la variable pour différents objets (approche un peu fonctionnelle). Dans ce cas, les blocs facilitent la séparation des tâches exécutées, à la fois syntaxiquement et visuellement. Il indique également qu'après le bloc, j'en ai fini avec il et rien ne devrait y accéder.

Un argument contre cet exemple est l'utilisation de méthodes. Peut-être que oui, mais dans ce cas, le code n'est pas si long et le séparer en différentes méthodes devrait également transmettre toutes les variables utilisées dans le code.