TL; DR - это эквивалентные примеры на уровне IL.
DotNetFiddle делает этот ответ довольно симпатичным, так как он позволяет увидеть итоговый IL.
Я использовал немного другой вариант вашей конструкции цикла, чтобы ускорить тестирование. Я использовал:
Вариация 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);
}
}
}
Вариация 2:
Console.WriteLine("Hello World");
int x;
for(x=0; x<=2; x++)
{
int i = x;
Console.WriteLine(i);
}
В обоих случаях скомпилированный вывод IL отображался одинаково.
.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
Итак, чтобы ответить на ваш вопрос: компилятор оптимизирует объявление переменной и делает два варианта эквивалентными.
Насколько я понимаю, компилятор .NET IL перемещает все объявления переменных в начало функции, но я не смог найти хороший источник, который четко заявил, что 2 . В этом конкретном примере вы видите, что он переместил их с этим утверждением:
.locals init (int32 V_0,
int32 V_1,
bool V_2)
При этом мы становимся слишком одержимыми в сравнении ...
Случай А, все переменные перемещены вверх?
Чтобы углубиться в это, я протестировал следующую функцию:
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);
}
}
Разница здесь в том , что мы объявляем либо int i
или на string j
основе сравнения. Опять же, компилятор перемещает все локальные переменные в верхнюю часть функции 2 с помощью:
.locals init (int32 V_0,
int32 V_1,
string V_2,
bool V_3)
Мне показалось интересным отметить, что, хотя int i
в этом примере и не будет объявлено, код для его поддержки все еще генерируется.
Случай B: А что foreach
вместо for
?
Было отмечено, что foreach
поведение отличается от того, for
и я не проверял то, о чем спрашивали. Поэтому я вставил эти два раздела кода, чтобы сравнить полученный IL.
int
объявление вне цикла:
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
объявление внутри цикла:
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);
}
Результирующий IL с foreach
петлей действительно отличался от IL, сгенерированного с помощью for
петли. В частности, блок инициализации и секция цикла изменились.
.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
foreach
Подход генерируется более локальных переменных , и требуется некоторое дополнительное разветвление. По сути, в первый раз он переходит к концу цикла, чтобы получить первую итерацию перечисления, а затем возвращается почти к вершине цикла, чтобы выполнить код цикла. Затем он продолжает проходить, как и следовало ожидать.
Но за ветвящееся различие , вызванным использования for
и foreach
конструкции, не была не разница в IL на основании которой int i
декларация была сделана. Таким образом, мы все еще находимся в двух подходах, являющихся эквивалентными.
Случай C: А как насчет разных версий компилятора?
В комментарии, который был оставлен 1 , была ссылка на вопрос SO, касающийся предупреждения о доступе к переменной с помощью foreach и использования замыкания . Часть, которая действительно привлекла мое внимание в этом вопросе, заключалась в том, что, возможно, были различия в работе компилятора .NET 4.5 по сравнению с более ранними версиями компилятора.
И вот где сайт DotNetFiddler подвел меня - все, что у них было доступно, - это .NET 4.5 и версия компилятора Roslyn. Поэтому я создал локальный экземпляр Visual Studio и начал тестировать код. Чтобы убедиться, что я сравнивал одни и те же вещи, я сравнил локально созданный код в .NET 4.5 с кодом DotNetFiddler.
Единственное отличие, которое я заметил, было в локальном блоке инициализации и объявлении переменной. Локальный компилятор был немного более конкретен в именовании переменных.
.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)
Но с этой незначительной разницей это было так хорошо, так хорошо. У меня был эквивалентный вывод IL между компилятором DotNetFiddler и тем, что производил мой локальный экземпляр VS.
Поэтому я перестроил проект, нацеленный на .NET 4, .NET 3.5, и, для хорошей оценки, на режим .NET 3.5 Release.
И во всех трех из этих дополнительных случаев сгенерированный IL был эквивалентен. Целевая версия .NET не влияла на IL, сгенерированный в этих примерах.
Подводя итог этому приключению: я думаю, что мы можем с уверенностью сказать, что компилятору не важно, где вы объявляете примитивный тип, и что это не влияет на память или производительность ни одним из методов объявления. И это справедливо независимо от использования цикла for
или foreach
.
Я рассмотрел запуск еще одного случая, который включал замыкание внутри foreach
цикла. Но вы спрашивали о влиянии того, где была объявлена переменная примитивного типа, поэтому я подумал, что я слишком далеко зашел над тем, о чем вы хотели спросить. На вопрос SO, который я упоминал ранее, есть отличный ответ, который дает хороший обзор эффектов замыкания на итерационные переменные foreach.
1 Спасибо Энди за предоставленную оригинальную ссылку на вопрос SO, посвященный замыканиям внутри foreach
циклов.
2 Стоит отметить, что в спецификации ECMA-335 это рассматривается в разделе I.12.3.2.2 «Локальные переменные и аргументы». Я должен был увидеть получившийся IL, а затем прочитать раздел, чтобы понять, что происходит. Спасибо фанатам за то, что указал на это в чате.