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 qui indique clairement que 2 . 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)
Où nous devenons un peu trop obsessionnels pour faire des comparaisons ...
Cas A, toutes les variables sont-elles déplacées vers le haut?
Pour creuser un peu plus, 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 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 fonction 2 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 s'il int i
ne sera pas déclaré dans cet exemple, le code pour le prendre en charge est toujours généré.
Cas B: Et au foreach
lieu de for
?
Il a été souligné que le foreach
comportement est différent de for
celui et que je ne vérifiais pas la même chose qui avait été posée. 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 foreach
boucle était en effet différent de l'IL généré en utilisant la for
boucle. 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' foreach
approche 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 en fonction de l'emplacement de la int i
déclaration. 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 était qu'il y avait peut-être 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 que je comparais les mêmes choses, j'ai comparé le code construit localement à .NET 4.5 au code DotNetFiddler.
La seule différence que j'ai notée était avec le bloc init 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 les 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é de lancer un autre cas qui incorporait une fermeture à l'intérieur de la foreach
boucle. Mais vous aviez posé des questions sur les effets de la déclaration d'une variable de type primitif, alors j'ai pensé que j'allais trop loin au-delà de ce que vous vouliez savoir. La question SO que j'ai mentionnée plus tôt a une excellente réponse qui fournit un bon aperçu des effets de fermeture sur les variables d'itération foreach.
1 Merci à Andy d'avoir fourni le lien d'origine vers la question SO traitant des fermetures dans les foreach
boucles.
2 Il convient de noter que la spécification ECMA-335 traite de cela avec la section I.12.3.2.2 «Variables et arguments locaux». J'ai dû voir l'IL obtenu et lire la section pour qu'il soit clair sur ce qui se passait. Merci à Ratchet Freak d'avoir signalé cela dans le chat.