Y a-t-il une différence entre ces deux versions de code?
foreach (var thing in things)
{
int i = thing.number;
// code using 'i'
// pay no attention to the uselessness of 'i'
}
int i;
foreach (var thing in things)
{
i = thing.number;
// code using 'i'
}
Ou le compilateur s'en fiche-t-il? Quand je parle de différence, je veux dire en termes de performances et d'utilisation de la mémoire. .. Ou fondamentalement n'importe quelle différence ou les deux finissent-ils par être le même code après la compilation?
TL; DR - ce sont des exemples équivalents au niveau de la couche IL.
DotNetFiddle rend cette jolie réponse car elle vous permet de voir l'IL résultant.
J'ai utilisé une variation légèrement différente de votre construction de boucle afin de rendre mes tests plus rapides. J'ai utilisé:
Variation 1:
using System;
public class Program
{
public static void Main()
{
Console.WriteLine("Hello World");
int x;
int i;
for(x=0; x<=2; x++)
{
i = x;
Console.WriteLine(i);
}
}
}
Variation 2:
Console.WriteLine("Hello World");
int x;
for(x=0; x<=2; x++)
{
int i = x;
Console.WriteLine(i);
}
Dans les deux cas, la sortie IL compilée est restée la même.
.class public auto ansi beforefieldinit Program
extends [mscorlib]System.Object
{
.method public hidebysig static void Main() cil managed
{
//
.maxstack 2
.locals init (int32 V_0,
int32 V_1,
bool V_2)
IL_0000: nop
IL_0001: ldstr "Hello World"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ldc.i4.0
IL_000d: stloc.0
IL_000e: br.s IL_001f
IL_0010: nop
IL_0011: ldloc.0
IL_0012: stloc.1
IL_0013: ldloc.1
IL_0014: call void [mscorlib]System.Console::WriteLine(int32)
IL_0019: nop
IL_001a: nop
IL_001b: ldloc.0
IL_001c: ldc.i4.1
IL_001d: add
IL_001e: stloc.0
IL_001f: ldloc.0
IL_0020: ldc.i4.2
IL_0021: cgt
IL_0023: ldc.i4.0
IL_0024: ceq
IL_0026: stloc.2
IL_0027: ldloc.2
IL_0028: brtrue.s IL_0010
IL_002a: ret
} // end of method Program::Main
Donc pour répondre à votre question: le compilateur optimise la déclaration de la variable, et rend les deux variantes équivalentes.
À ma connaissance, le compilateur .NET IL déplace toutes les déclarations de variables au début de la fonction, mais je n'ai pas pu trouver une bonne source indiquant clairement que2. Dans cet exemple particulier, vous voyez qu'il les a déplacés vers le haut avec cette instruction:
.locals init (int32 V_0,
int32 V_1,
bool V_2)
Cas A, toutes les variables sont-elles déplacées vers le haut?
Pour creuser un peu plus loin, j'ai testé la fonction suivante:
public static void Main()
{
Console.WriteLine("Hello World");
int x=5;
if (x % 2==0)
{
int i = x;
Console.WriteLine(i);
}
else
{
string j = x.ToString();
Console.WriteLine(j);
}
}
La différence ici est que nous déclarons soit un int i
ou un string j
basé sur la comparaison. Encore une fois, le compilateur déplace toutes les variables locales en haut de la fonction2 avec:
.locals init (int32 V_0,
int32 V_1,
string V_2,
bool V_3)
J'ai trouvé intéressant de noter que même si int i
ne sera pas déclaré dans cet exemple, le code pour le supporter est toujours généré.
Cas B: Qu'en est-il de foreach
au lieu de for
?
Il a été souligné que foreach
a un comportement différent de for
et que je ne vérifiais pas la même chose que ce qui avait été demandé. J'ai donc mis ces deux sections de code pour comparer l'IL résultant.
int
déclaration en dehors de la boucle:
Console.WriteLine("Hello World");
List<int> things = new List<int>(){1, 2, 3, 4, 5};
int i;
foreach(var thing in things)
{
i = thing;
Console.WriteLine(i);
}
int
déclaration à l'intérieur de la boucle:
Console.WriteLine("Hello World");
List<int> things = new List<int>(){1, 2, 3, 4, 5};
foreach(var thing in things)
{
int i = thing;
Console.WriteLine(i);
}
L'IL résultant avec la boucle foreach
était en effet différent de l'IL généré en utilisant la boucle for
. Plus précisément, le bloc init et la section de boucle ont changé.
.locals init (class [mscorlib]System.Collections.Generic.List`1<int32> V_0,
int32 V_1,
int32 V_2,
class [mscorlib]System.Collections.Generic.List`1<int32> V_3,
valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> V_4,
bool V_5)
...
.try
{
IL_0045: br.s IL_005a
IL_0047: ldloca.s V_4
IL_0049: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
IL_004e: stloc.1
IL_004f: nop
IL_0050: ldloc.1
IL_0051: stloc.2
IL_0052: ldloc.2
IL_0053: call void [mscorlib]System.Console::WriteLine(int32)
IL_0058: nop
IL_0059: nop
IL_005a: ldloca.s V_4
IL_005c: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
IL_0061: stloc.s V_5
IL_0063: ldloc.s V_5
IL_0065: brtrue.s IL_0047
IL_0067: leave.s IL_0078
} // end .try
finally
{
IL_0069: ldloca.s V_4
IL_006b: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
IL_0071: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0076: nop
IL_0077: endfinally
} // end handler
L'approche foreach
a généré plus de variables locales et a nécessité des branchements supplémentaires. Essentiellement, la première fois, il saute à la fin de la boucle pour obtenir la première itération de l'énumération, puis revient presque au sommet de la boucle pour exécuter le code de la boucle. Il continue ensuite à boucler comme prévu.
Mais au-delà des différences de branchement causées par l'utilisation des constructions for
et foreach
, il n'y avait aucune différence dans l'IL basé où le int i
la déclaration a été placée. Nous sommes donc toujours aux deux approches étant équivalentes.
Cas C: Qu'en est-il des différentes versions du compilateur?
Dans un commentaire qui a été laissé1, il y avait un lien vers une question SO concernant un avertissement concernant l'accès variable avec foreach et l'utilisation de la fermeture . La partie qui a vraiment attiré mon attention dans cette question est qu'il peut y avoir des différences dans le fonctionnement du compilateur .NET 4.5 par rapport aux versions antérieures du compilateur.
Et c'est là que le site DotNetFiddler m'a laissé tomber - tout ce qu'ils avaient à disposition était .NET 4.5 et une version du compilateur Roslyn. J'ai donc mis en place une instance locale de Visual Studio et commencé à tester le code. Pour m'assurer de comparer les mêmes choses, j'ai comparé le code construit localement en .NET 4.5 au code DotNetFiddler.
La seule différence que j'ai notée était avec le bloc d'initialisation local et la déclaration de variable. Le compilateur local était un peu plus spécifique pour nommer les variables.
.locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> things,
[1] int32 thing,
[2] int32 i,
[3] class [mscorlib]System.Collections.Generic.List`1<int32> '<>g__initLocal0',
[4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> CS$5$0000,
[5] bool CS$4$0001)
Mais avec cette petite différence, c'était si loin, tellement bon. J'avais une sortie IL équivalente entre le compilateur DotNetFiddler et ce que mon instance VS locale produisait.
J'ai donc reconstruit le projet en ciblant .NET 4, .NET 3.5, et pour faire bonne mesure .NET 3.5 Release mode.
Et dans ces trois cas supplémentaires, l'IL généré était équivalent. La version .NET ciblée n'a eu aucun effet sur l'IL généré dans ces échantillons.
Pour résumer cette aventure: Je pense que nous pouvons dire avec confiance que le compilateur ne se soucie pas de l'endroit où vous déclarez le type primitif et qu'il n'y a aucun effet sur la mémoire ou performances avec l'une ou l'autre méthode de déclaration. Et cela reste vrai indépendamment de l'utilisation d'une boucle for
ou foreach
.
J'ai envisagé d'exécuter un autre cas qui incorporait une fermeture à l'intérieur de la boucle foreach
. Mais vous aviez posé des questions sur les effets de l'endroit où une variable de type primitif a été déclarée, alors j'ai pensé que j'allais trop loin au-delà de ce que vous vouliez demander. La question SO que j'ai mentionnée plus tôt a un excellente réponse qui donne un bon aperçu des effets de fermeture sur les variables d'itération foreach.
1 Merci à Andy d'avoir fourni le lien original vers la SO question adressant les fermetures dans les boucles foreach
.
2 Il convient de noter que la spécification ECMA-335 aborde cela avec la section I.12.3.2.2 "Variables et arguments locaux". J'ai dû voir l'IL obtenu, puis lire la section pour qu'il soit clair concernant ce qui se passait. Merci à Ratchet Freak d'avoir signalé cela dans le chat.
Selon le compilateur que vous utilisez (je ne sais même pas si C # en a plusieurs), votre code sera optimisé avant d'être transformé en programme. Un bon compilateur verra que vous réinitialisez la même variable à chaque fois avec une valeur différente et gérez efficacement l'espace mémoire pour cela.
Si vous initialisiez la même variable à une constante à chaque fois, le compilateur l'initierait également avant la boucle et la référencerait.
Tout dépend de la qualité de l'écriture de votre compilateur, mais en ce qui concerne les normes de codage, les variables doivent toujours avoir la portée la moins possible . Donc, déclarer à l'intérieur de la boucle est ce que j'ai toujours appris.