これらの2つのバージョンのコードに違いはありますか?
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'
}
またはコンパイラは気にしませんか?違いといえば、パフォーマンスとメモリ使用量のことです。 ..または、基本的に少しだけ違いがありますか、それともコンパイル後に2つが同じコードになるのですか?
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
したがって、あなたの質問に答えるために:コンパイラは変数の宣言を最適化し、2つのバリエーションを同等にレンダリングします
私の理解では、.NET ILコンパイラはすべての変数宣言を関数の先頭に移動しますが、次のことを明確に述べている適切なソースを見つけることができませんでした。2。この特定の例では、次のステートメントでそれらを上に移動したことがわかります。
.locals init (int32 V_0,
int32 V_1,
bool V_2)
ケースA、すべての変数が上に移動しますか?
これをさらに掘り下げるために、次の関数をテストしました。
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を比較するために、これら2つのコードセクションを挿入しました。
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
ループを使用して生成されたILは、実際にfor
ループを使用して生成されたILとは異なりました。具体的には、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ベースの違いはnoint i
宣言が配置された場所。したがって、2つのアプローチは同等です。
ケースC:異なるコンパイラバージョンはどうですか?
残されたコメント内1、 foreachとクロージャーの使用による変数アクセスに関する警告に関するSOの質問 へのリンクがありました。この質問で本当に私の目を引いたのは、.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)
しかし、そのわずかな違いで、それはこれまでのところ、とても良かったです。 DotNetFiddlerコンパイラーとローカルVSインスタンスが生成しているものとの間に同等のIL出力がありました。
そこで、.NET 4、.NET 3.5、および.NET 3.5リリースモードを対象としたプロジェクトを再構築しました。
これら3つの追加のケースすべてで、生成されたILは同等でした。対象の.NETバージョンは、これらのサンプルで生成されたILに影響を与えませんでした。
この冒険を要約すると:私は、コンパイラがプリミティブ型を宣言する場所を気にせず、メモリまたはいずれかの宣言方法でのパフォーマンス。そして、それはfor
またはforeach
ループの使用に関係なく当てはまります。
foreach
ループ内にクロージャーを組み込んださらに別のケースを実行することを検討しました。しかし、プリミティブ型変数が宣言されている場所の影響について尋ねたので、私があなたが尋ねることに興味があることをはるかに超えて掘り下げていると思いました。 SO先ほど述べた質問には 素晴らしい答え があり、foreach反復変数に対するクロージャの影響についての概要がわかります。
1SO foreach
ループ内のクロージャに対処する質問への元のリンクを提供してくれたAndyに感謝します。
2ECMA-335仕様 がセクションI.12.3.2.2「ローカル変数と引数」でこれに対処していることに注目する価値があります。結果のILを確認し、セクションを読んで何が起こっているのかを明確にする必要がありました。チャットで指摘してくれたラチェットフリークに感謝します。
使用するコンパイラに応じて(C#に複数あるかどうかもわかりません)、コードはプログラムに変換される前に最適化されます。優れたコンパイラーは、毎回同じ変数を異なる値で再初期化していることを確認し、そのメモリー空間を効率的に管理します。
同じ変数を毎回定数に初期化する場合、コンパイラーも同様に変数を初期化しますbeforeループを参照します。
それはすべて、コンパイラがどの程度適切に記述されているかによって異なりますが、コーディング標準に関する限り、変数は常に可能な限り小さいスコープを持つ必要があります。したがって、ループの内側で宣言することは、私が常に教えてきたものです。