TL; DR - вони еквівалентні приклади на рівні IL.
DotNetFiddle змушує це відповісти, оскільки дозволяє побачити отриманий ІЛ.
Я скористався дещо іншим варіантом конструкції вашого циклу, щоб зробити тестування швидшим. Я використав:
Варіант 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
те, що я не перевіряв те саме, про що питали. Тому я вкладаю в ці два розділи коду для порівняння отриманого ІЛ.
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);
}
Отриманий ІЛ з foreach
циклом справді відрізнявся від ІЛ, сформованого за допомогою for
циклу. Зокрема, змінився блок init та розділ циклу.
.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 , знайшлося посилання на запитання про те, що стосується попередження про змінний доступ із функцією foreach та використання закриття . Частина, яка насправді зачепила мене в цьому питанні, полягала в тому, що, можливо, існували відмінності в тому, як компілятор .NET 4.5 працював проти попередніх версій компілятора.
І саме тут сайт DotNetFiddler мене підвів - все, що вони мали, -. NET 4.5 та версія компілятора Roslyn. Тому я вивів локальний екземпляр Visual Studio і почав тестувати код. Щоб переконатися, що я порівнював ті самі речі, я порівняв локально вбудований код у .NET 4.5 з кодом DotNetFiddler.
Єдина відмінність, яку я зазначив, була в локальному блоці init та оголошенні змінної. Локальний компілятор був дещо конкретнішим у назві змінних.
.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.
І в усіх трьох цих додаткових випадках генерований ІЛ був рівноцінним. Цільова версія .NET не впливала на ІЛ, що генерується в цих зразках.
Підсумовуючи цю пригоду: я думаю, що ми можемо з упевненістю сказати, що компілятору не важливо, де ви оголосили примітивний тип, і що жодним із способів декларування не впливає на пам'ять чи продуктивність. І це справедливо незалежно від використання for
або foreach
циклу.
Я розглядав запуск ще одного випадку, який включав закриття всередині foreach
циклу. Але ви запитували про наслідки декларування примітивної змінної типу, тож я зрозумів, що я заглиблююся далеко за рамки того, про що ви зацікавлені. Питання ТА, про яке я згадував раніше, має чудову відповідь, яка забезпечує хороший огляд ефектів закриття на змінні ітерації випередження.
1 Дякую Енді, що ви надали оригінальне посилання на питання SO, яке стосується закриттів в foreach
циклі.
2 Варто зазначити, що специфікація ECMA-335 вирішує це у розділі I.12.3.2.2 "Локальні змінні та аргументи". Мені довелося побачити отриманий ІЛ, а потім прочитати розділ, щоб зрозуміти, що відбувається. Завдяки щурячому виродку за вказівку на це в чаті.