De nombreux langages de programmation "de type C" utilisent des instructions composées (blocs de code spécifiés avec "{" et "}") pour définir une étendue de variables.
Voici un exemple simple.
for (int i = 0; i < 100; ++i) {
int value = function(i); // Here 'value' is local to this block
printf("function(%d) == %d\n", i, value);
}
C'est bien car cela limite la portée de value
à l'endroit où il est utilisé. Il est difficile pour les programmeurs d'utiliser value
d'une manière qui ne leur est pas destinée car ils ne peuvent y accéder qu'à partir de sa portée.
Je suis presque tous conscients de cela et convenez qu'il est de bonne pratique de déclarer des variables dans le bloc qu'elles sont utilisées pour limiter leur portée.
Mais même s'il s'agit d'une convention établie pour déclarer des variables dans leur portée la plus petite possible, il n'est pas très courant d'utiliser une instruction composée nue (c'est-à-dire une instruction composée qui n'est pas connectée à un if
, for
, while
instruction).
Les programmeurs écrivent souvent le code comme ceci:
int x = ???
int y = ???
// Swap `x` and `y`
int tmp = x;
x = y;
y = tmp;
Ne serait-il pas préférable d'écrire le code comme ceci:
int x = ???
int y = ???
// Swap `x` and `y`
{
int tmp = x;
x = y;
y = tmp;
}
Cela semble assez moche, mais je pense que c'est un bon moyen d'appliquer la localité variable et de rendre le code plus sûr à utiliser.
Je vois souvent des modèles similaires où une variable est utilisée une fois dans une fonction
Object function(ParameterType arg) {
Object obj = new Object(obj);
File file = File.open("output.txt", "w+");
file.write(obj.toString());
// `obj` is used more here but `file` is never used again.
...
}
Pourquoi ne l'écrivons-nous pas ainsi?
RET_TYPE function(PARAM_TYPE arg) {
Object obj = new Object(obj);
{
File file = File.open("output.txt", "w+");
file.write(obj.toString());
}
// `obj` is used more here but `file` is never used again.
...
}
Il est difficile de trouver de bons exemples. Je suis sûr qu'il existe de meilleures façons d'écrire le code dans mes exemples, mais ce n'est pas de cela qu'il s'agit.
Ma question est de savoir pourquoi nous n'utilisons pas plus d'instructions composées "nues" pour limiter la portée des variables.
Que pensez-vous de l'utilisation d'une instruction composée comme celle-ci
{
int tmp = x;
x = y;
y = z;
}
limiter la portée de tmp
?
Est-ce une bonne pratique? Est-ce une mauvaise pratique? Expliquez vos pensées.
Le code suivant Java Java montre ce que je pense être l'un des meilleurs exemples de la façon dont les blocs nus peuvent être utiles.
Comme vous pouvez le voir, la méthode compareTo()
a trois comparaisons à faire, et les résultats des deux premiers doivent être stockés temporairement dans un local. Le local n'est qu'une `` différence '' dans les deux cas, mais réutiliser la même variable locale est une mauvaise idée, et en fait sur une décente IDE vous pouvez configurer la réutilisation des variables locales pour provoquer un avertissement.
class MemberPosition implements Comparable<MemberPosition>
{
final int derivationDepth;
final int lineNumber;
final int columnNumber;
MemberPosition( int derivationDepth, int lineNumber, int columnNumber )
{
this.derivationDepth = derivationDepth;
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
}
@Override
public int compareTo( MemberPosition o )
{
/* first, compare by derivation depth, so that all ancestor methods will be executed before all descendant methods. */
{
int d = Integer.compare( derivationDepth, o.derivationDepth );
if( d != 0 )
return d;
}
/* then, compare by line number, so that methods will be executed in the order in which they appear in the source file. */
{
int d = Integer.compare( lineNumber, o.lineNumber );
if( d != 0 )
return d;
}
/* finally, compare by column number. You know, just in case you have multiple test methods on the same line. Whatever. */
return Integer.compare( columnNumber, o.columnNumber );
}
}
Notez comment dans ce cas particulier vous ne pouvez pas décharger le travail vers une fonction distincte, comme le suggère la réponse de Kilian Foth. Donc, dans des cas comme celui-ci, les blocs nus sont toujours ma préférence. Mais même dans les cas où vous pouvez en fait déplacer le code vers une fonction distincte, je préfère a) garder les choses au même endroit afin de minimiser le défilement nécessaire pour donner un sens à un morceau de code, et b) ne pas gonfler mon code avec beaucoup de fonctions. Certainement une bonne pratique.
(Note latérale: l'une des raisons pour lesquelles le style de crochet bouclé égyptien est vraiment nul, c'est qu'il ne fonctionne pas avec des blocs nus.)
(Une autre note latérale dont je me souvenais maintenant, un mois plus tard: le code ci-dessus est en Java, mais j'ai en fait repris l'habitude de mes jours de C++, où le crochet bouclé de fermeture provoque l'invocation de destructeurs. C'est le bit RAII qui rachet freak mentionne également dans sa réponse, et ce n'est pas seulement une bonne chose, c'est à peu près indispensable.)
C'est en effet une bonne pratique de garder la portée de votre variable petite. Cependant, l'introduction de blocs anonymes dans de grandes méthodes ne résout que la moitié du problème: la portée des variables diminue, mais la méthode augmente (légèrement)!
La solution est évidente: ce que vous vouliez faire dans un bloc anonyme, vous devriez le faire dans une méthode. La méthode obtient automatiquement son propre bloc et sa propre portée, et avec un nom significatif, vous obtenez également une meilleure documentation.
Souvent, si vous trouvez des endroits pour créer une telle étendue, c'est l'occasion d'extraire une fonction.
Dans une langue avec pass-by-reference, vous appelleriez à la place swap(x,y)
.
Pour avoir écrit le fichier, il serait conseillé d'utiliser le bloc pour s'assurer que RAII fermera le fichier et libérera les ressources dès que possible.
Je suppose que vous ne connaissez pas encore Expression-Oriented languages?
Dans les langages orientés expression, (presque) tout est une expression. Cela signifie, par exemple, qu'un bloc peut être une expression comme dans Rust:
// A typical function displaying x^3 + x^2 + x
fn typical_print_x3_x2_x(x: i32) {
let y = x * x * x + x * x + x;
println!("{}", y);
}
vous craignez peut-être que le compilateur calcule de manière redondante x * x
cependant, et a décidé de mémoriser le résultat:
// A memoizing function displaying x^3 + x^2 + x
fn memoizing_print_x3_x2_x(x: i32) {
let x2 = x * x;
let y = x * x2 + x2 + x;
println!("{}", y);
}
Cependant, maintenant x2
survit clairement à son utilité. Eh bien, dans Rust un bloc est une expression qui retourne la valeur de sa dernière expression (pas de sa dernière instruction, donc évitez une fermeture ;
):
// An expression-oriented function displaying x^3 + x^2 + x
fn expr_print_x3_x2_x(x: i32) {
let y = {
let x2 = x * x;
x * x2 + x2 + x // <- missing semi-colon, this is an expression
};
println!("{}", y);
}
Je dirais donc que les langues plus récentes ont reconnu l'importance de limiter la portée des variables (rend les choses plus propres) et offrent de plus en plus de facilités pour le faire.
Même des experts C++ notables tels que Herb Sutter recommandent des blocs anonymes de ce type, "piratant" des lambdas pour initialiser des variables constantes (car immuable est génial):
int32_t const y = [&]{
int32_t const x2 = x * x;
return x * x2 + x2 + x;
}(); // do not forget to actually invoke the lambda with ()
Félicitations, vous avez l'isolement de la portée de certaines variables triviales dans une fonction large et complexe.
Malheureusement, vous avez une fonction vaste et complexe. La bonne chose à faire est au lieu de créer une étendue dans la fonction de la variable, de l'extraire dans sa propre fonction. Cela encourage la réutilisation du code et permet à la portée entourant d'être transmise en tant que paramètres à la fonction et non comme des variables pseudo-globales pour cette portée anonyme.
Cela signifie que tout ce qui est en dehors de la portée du bloc sans nom est toujours dans la portée du bloc sans nom. Vous programmez efficacement avec des globaux et des fonctions sans nom exécutées en série sans aucun moyen de réutiliser le code.
En outre, considérez que dans de nombreux runtimes, all la déclaration de variable dans les étendues anonymes est déclarée et allouée en haut de la méthode.
Regardons quelques C # ( https://dotnetfiddle.net/QKtaG4 ):
using System;
public class Program
{
public static void Main()
{
string h = "hello";
string w = "world";
{
int i = 42;
Console.WriteLine(i);
}
{
int j = 4;
Console.WriteLine(j);
}
Console.WriteLine(h + w);
}
}
Et lorsque vous commencez à creuser dans l'IL pour le code (avec dotnetfiddle, sous 'tidy up' il y a aussi 'View IL'), juste en haut, il montre ce qui a été alloué pour cette méthode:
.class public auto ansi beforefieldinit Program
extends [mscorlib]System.Object
{
.method public hidebysig static void Main() cli managed
{
//
.maxstack 2
.locals init (string V_0,
string V_1,
int32 V_2,
int32 V_3)
Cela alloue l'espace pour deux chaînes et deux entiers lors de l'initialisation de la méthode Main, même si un seul int32 est dans la portée à un moment donné. Vous pouvez voir une analyse plus approfondie de ceci sur un sujet tangentiel d'initialisation de variables à boucle Foreach et initialisation de variable .
Regardons plutôt du code Java à la place. Cela devrait sembler assez familier.
public class Main {
public static void main(String[] args) {
String h = "hello";
String w = "world";
{
int i = 42;
System.out.println(i);
}
{
int j = 4;
System.out.println(j);
}
System.out.println(h + w);
}
}
Compilez cela avec javac, puis appelez javap -v Main.class
et vous obtenez désassembleur du fichier de classe Java .
Juste en haut, il vous indique le nombre d'emplacements dont il a besoin pour les variables locales ( sortie de la commande javap va dans l'explication des parties un peu plus):
public static void main(Java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
Il a besoin de quatre emplacements. Même si deux de ces variables locales ne sont jamais dans le champ d'application en même temps, il encore alloue de la place pour quatre variables locales.
D'un autre côté, si vous donnez au compilateur l'option, extraire des méthodes courtes (comme un "swap") fera un meilleur travail d'inliner la méthode et de faire une utilisation plus optimale des variables.
Bonne pratique. Période. Point final. Vous expliquez aux futurs lecteurs que la variable n'est utilisée nulle part ailleurs, imposée par les règles du langage.
C'est une politesse provisoire pour les futurs responsables, car vous leur dites quelque chose que vous savez. Ces faits pourraient prendre des dizaines de minutes à déterminer dans une fonction suffisamment longue ou alambiquée.
Les secondes que vous passerez à documenter ce fait puissant en ajoutant une paire d'accolades rapporteront des minutes à chaque responsable qui n'aura pas à déterminer ce fait.
Le futur mainteneur peut être vous-même, des années plus tard, ( parce que vous êtes le seul à connaître ce code ) avec d'autres choses en tête ( parce que le projet est terminé depuis longtemps et que vous avez été réaffecté depuis ), et sous pression. ( parce que nous le faisons gracieusement pour le client, vous savez, ils sont des partenaires de longue date, vous savez, et nous avons dû lisser notre relation commerciale avec eux parce que le dernier projet nous n'avons pas répondu aux attentes, vous savez, et oh, c'est juste un petit changement et cela ne devrait pas prendre autant de temps pour un expert comme vous, et nous n'avons pas de budget alors ne faites pas de "sur-qualité" )
Tu te détesterais de ne pas le faire.
Oui, les blocs nus peuvent limiter les portées variables plus que vous ne les limiteriez normalement. Toutefois:
Le seul gain est un précédent fin de la durée de vie des variables; vous pouvez facilement et de manière totalement discrète limiter le début de sa durée de vie en déplaçant la déclaration vers le bas à l'endroit approprié. C'est une pratique courante, nous sommes donc déjà à mi-chemin de l'endroit où vous pouvez obtenir avec des blocs nus, et cette moitié est gratuite.
En règle générale, chaque variable a sa propre place où elle cesse d'être utile; tout comme chaque variable a sa propre place où elle doit être déclarée. Donc, si vous vouliez limiter autant que possible toutes les étendues de variables, vous vous retrouveriez avec un bloc pour chaque variable dans de nombreux cas.
Un bloc nu introduit structure supplémentaire à la fonction, ce qui rend son fonctionnement plus difficile à saisir. Avec le point 2, cela peut rendre une fonction avec des étendues entièrement restreintes presque illisible.
Donc, je suppose que l'essentiel est que les blocs nus ne sont tout simplement pas rentables dans la grande majorité des cas. Il y a des cas où un bloc nu peut avoir du sens parce que vous devez contrôler où un destructeur est appelé, ou parce que vous avez plusieurs variables avec une fin d'utilisation presque identique. Pourtant, surtout dans le dernier cas, vous devez absolument penser à intégrer le bloc dans sa propre fonction. Les situations qui sont mieux résolues par un bloc nu sont extrêmement rares.
Oui, les blocs nus sont très rarement vus, et je pense que c'est parce qu'ils sont très rarement nécessaires.
Un endroit où je les utilise moi-même est dans les instructions switch:
case 'a': {
int x = getAmbientTemperature();
int y = getBackgroundIllumination();
setVasculosity(x * y);
break;
}
case 'b': {
int x = getUltrification();
int y = getMendacity();
setVasculosity(x + y);
break;
}
J'ai tendance à le faire chaque fois que je déclare des variables dans les branches d'un commutateur, car cela garde les variables hors de portée des branches suivantes.
Des blocs/étendues supplémentaires ajoutent un verbiage supplémentaire. Bien que l'idée présente un attrait initial, vous rencontrerez bientôt des situations où elle devient de toute façon irréalisable car vous avez des variables temporaires avec une étendue chevauchement qui ne peuvent pas être correctement reflétées par une structure de bloc.
Donc, comme vous ne pouvez pas faire en sorte que la structure de bloc reflète de manière cohérente des durées de vie variables dès que les choses deviennent un peu plus complexes, se pencher en arrière pour les cas simples où cela fonctionne semble être un exercice de futilité éventuelle.
Pour les structures de données avec des destructeurs qui bloquent des ressources importantes pendant leur durée de vie, il est possible d'appeler le destructeur de manière explicite. Contrairement à l'utilisation d'une structure en blocs, cela ne requiert pas un ordre de destruction particulier pour les variables introduites à différents moments.
Bien sûr, les blocs ont leur utilité lorsqu'ils sont liés à des unités logiques: en particulier lors de l'utilisation de la programmation de macro, la portée de toute variable non explicitement nommée comme argument de macro destiné à la sortie est mieux limitée au corps de macro lui-même afin de ne pas provoquer de surprises lors de l'utilisation une macro plusieurs fois.
Mais en tant que marqueurs à vie pour les variables en exécution séquentielle, les blocs ont tendance à être excessifs.