5

プロダクションで奇妙なバグが発生しており、調査するように依頼されました。
この問題は、For ループ内で宣言され、各反復で初期化されていないいくつかの変数にまで追跡されました。宣言の範囲により、反復ごとに「リセット」されるという前提がありました。
誰かがなぜそうならないのか説明できますか?)
(私の最初の質問です。回答を本当に楽しみにしています。)
以下の例は明らかに問題のコードではありませんが、シナリオを反映しています:
コード例を許してください。エディターのプレビューでは問題ないように見えますか??

for (int i =0; i< 10; i++)
{
    decimal? testDecimal;
    string testString;

    switch( i % 2  )
    {
        case 0:
        testDecimal = i / ( decimal ).32;
        testString = i.ToString();
            break;
        default:
            testDecimal = null;
            testString = null;
            break;
    }

    Console.WriteLine( "Loop {0}: testDecimal={1} - testString={2}", i, testDecimal , testString );
}

編集:

申し訳ありませんが、育児問題のために急いで行かなければなりませんでした。問題は、prod コードにあったのは、switch ステートメントが巨大で、if (myObject.Prop != null) then testString = myObject.Stringval のように、いくつかの「ケース」でクラスのプロパティのチェックが行われていたことでした。 .. スイッチの最後で、(外部で) testString == null のチェックが行われていましたが、最後の反復からの値が保持されていたため、コーダーがループ内で宣言されている変数で想定したように null ではありませんでした。
私の質問と例が少しずれていたらすみません、デイケアについて一緒に叩いていたときに電話がありました。ループの内外で両方の変数から IL を比較したことを言及しておく必要がありました。では、「ループごとに変数が再初期化されないことは明らかだ」という一般的な意見はありますか?
もう少し情報、変数 WHERE は、ReSharper が「値は決して使用されない」と指摘してそれらを削除することに熱狂するまで、反復ごとに初期化されます。


編集:

皆さん、ありがとうございました。私の最初の投稿として、将来どれだけ明確にする必要があるかがわかります。予期しない変数割り当ての原因は、経験の浅い開発者が ReSharper から指示されたすべてのことを実行し、ソリューション全体で「コードのクリーンアップ」を実行した後、単体テストを実行していないことに起因する可能性があります。VSS でこのモジュールの履歴を見ると、変数がループの外で宣言され、各反復で初期化されていることがわかります。問題の人は、ReSharper が「すべて緑色」を表示することを望んでいたため、「変数を割り当てに近づけて」、「冗長な割り当てを削除しました」! 私は彼がそれを再びやるとは思わない.今週末は彼が逃したすべての単体テストを実行するために費やす!
質問を回答済みとしてマークするにはどうすればよいですか?

4

7 に答える 7

15

ほとんどの場合、変数をループ内で宣言するかループ外で宣言するかは問題ではありません。明確な割り当ての規則により、それが問題にならないことが保証されます。デバッガーでは、古い値が表示されることがありますが (つまり、変数が割り当てられる前にブレークポイントで変数を調べた場合など)、静的分析により、これがコードの実行に影響を与えないことが証明されています。明らかに必要がないため、ループごとに変数がリセットされることはありません。

IL レベルでは、**通常* 変数はメソッドに対して 1 回だけ宣言されます。ループ内の配置は、私たちプログラマーにとって便利です。

ただし、重要な例外があります変数がキャプチャされるたびに、スコープ ルールはより複雑になります。例 (2 秒):

        int value;
        for (int i = 0; i < 5; i++)
        {
            value = i;
            ThreadPool.QueueUserWorkItem(delegate { Console.WriteLine(value); });
        }
        Console.ReadLine();

次のものとは大きく異なります。

        for (int i = 0; i < 5; i++)
        {
            int value = i;
            ThreadPool.QueueUserWorkItem(delegate { Console.WriteLine(value); });
        }
        Console.ReadLine();

2 番目の例の「値」は、キャプチャされるため、実際にはインスタンスごとです。つまり、最初の例では (たとえば) "4 4 4 4 4" が表示されますが、2 番目の例では 0-5 (任意の順序) が表示されます。つまり、"1 2 5 3 4" です。

では、キャプチャは元のコードに含まれていたのでしょうか? ラムダ、匿名メソッド、または LINQ クエリを使用するものはすべて対象となります。

于 2008-12-05T23:40:19.370 に答える
14

概要

ループ内で変数を宣言するために生成された IL と、ループ外で変数を宣言するために生成された IL を比較すると、2 つのスタイルの変数宣言の間にパフォーマンスの違いがないことが証明されます。(生成された IL は実質的に同一です。)


変数がループ内で宣言されているため、おそらく「より多くのリソース」を使用している元のソースは次のとおりです。

using System;

class A
{
    public static void Main()
    {
        for (int i =0; i< 10; i++)
        {
            decimal? testDecimal;
            string testString;
            switch( i % 2  )
            {
                case 0:
                    testDecimal = i / ( decimal ).32;
                    testString = i.ToString();
                    break;
                default:
                    testDecimal = null;
                    testString = null;
                    break;
            }

            Console.WriteLine( "Loop {0}: testDecimal={1} - testString={2}", i, testDecimal , testString );
        }
    }
}

非効率的な宣言ソースからの IL は次のとおりです。

.method public hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 8
    .locals init (
        [0] int32 num,
        [1] valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal> nullable,
        [2] string str,
        [3] int32 num2,
        [4] bool flag)
    L_0000: nop 
    L_0001: ldc.i4.0 
    L_0002: stloc.0 
    L_0003: br.s L_0061
    L_0005: nop 
    L_0006: ldloc.0 
    L_0007: ldc.i4.2 
    L_0008: rem 
    L_0009: stloc.3 
    L_000a: ldloc.3 
    L_000b: ldc.i4.0 
    L_000c: beq.s L_0010
    L_000e: br.s L_0038
    L_0010: ldloca.s nullable
    L_0012: ldloc.0 
    L_0013: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(int32)
    L_0018: ldc.i4.s 0x20
    L_001a: ldc.i4.0 
    L_001b: ldc.i4.0 
    L_001c: ldc.i4.0 
    L_001d: ldc.i4.2 
    L_001e: newobj instance void [mscorlib]System.Decimal::.ctor(int32, int32, int32, bool, uint8)
    L_0023: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Division(valuetype [mscorlib]System.Decimal, valuetype [mscorlib]System.Decimal)
    L_0028: call instance void [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal>::.ctor(!0)
    L_002d: nop 
    L_002e: ldloca.s num
    L_0030: call instance string [mscorlib]System.Int32::ToString()
    L_0035: stloc.2 
    L_0036: br.s L_0044
    L_0038: ldloca.s nullable
    L_003a: initobj [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal>
    L_0040: ldnull 
    L_0041: stloc.2 
    L_0042: br.s L_0044
    L_0044: ldstr "Loop {0}: testDecimal={1} - testString={2}"
    L_0049: ldloc.0 
    L_004a: box int32
    L_004f: ldloc.1 
    L_0050: box [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal>
    L_0055: ldloc.2 
    L_0056: call void [mscorlib]System.Console::WriteLine(string, object, object, object)
    L_005b: nop 
    L_005c: nop 
    L_005d: ldloc.0 
    L_005e: ldc.i4.1 
    L_005f: add 
    L_0060: stloc.0 
    L_0061: ldloc.0 
    L_0062: ldc.i4.s 10
    L_0064: clt 
    L_0066: stloc.s flag
    L_0068: ldloc.s flag
    L_006a: brtrue.s L_0005
    L_006c: ret 
}

ループ外で変数を宣言するソースは次のとおりです。

using System;

class A
{
    public static void Main()
    {
        decimal? testDecimal;
        string testString;

        for (int i =0; i< 10; i++)
        {
            switch( i % 2  )
            {
                case 0:
                    testDecimal = i / ( decimal ).32;
                    testString = i.ToString();
                    break;
                default:
                    testDecimal = null;
                    testString = null;
                    break;
            }

            Console.WriteLine( "Loop {0}: testDecimal={1} - testString={2}", i, testDecimal , testString );
        }
    }
}

ループ外で変数を宣言する IL は次のとおりです。

.method public hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 8
    .locals init (
        [0] valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal> nullable,
        [1] string str,
        [2] int32 num,
        [3] int32 num2,
        [4] bool flag)
    L_0000: nop 
    L_0001: ldc.i4.0 
    L_0002: stloc.2 
    L_0003: br.s L_0061
    L_0005: nop 
    L_0006: ldloc.2 
    L_0007: ldc.i4.2 
    L_0008: rem 
    L_0009: stloc.3 
    L_000a: ldloc.3 
    L_000b: ldc.i4.0 
    L_000c: beq.s L_0010
    L_000e: br.s L_0038
    L_0010: ldloca.s nullable
    L_0012: ldloc.2 
    L_0013: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(int32)
    L_0018: ldc.i4.s 0x20
    L_001a: ldc.i4.0 
    L_001b: ldc.i4.0 
    L_001c: ldc.i4.0 
    L_001d: ldc.i4.2 
    L_001e: newobj instance void [mscorlib]System.Decimal::.ctor(int32, int32, int32, bool, uint8)
    L_0023: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Division(valuetype [mscorlib]System.Decimal, valuetype [mscorlib]System.Decimal)
    L_0028: call instance void [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal>::.ctor(!0)
    L_002d: nop 
    L_002e: ldloca.s num
    L_0030: call instance string [mscorlib]System.Int32::ToString()
    L_0035: stloc.1 
    L_0036: br.s L_0044
    L_0038: ldloca.s nullable
    L_003a: initobj [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal>
    L_0040: ldnull 
    L_0041: stloc.1 
    L_0042: br.s L_0044
    L_0044: ldstr "Loop {0}: testDecimal={1} - testString={2}"
    L_0049: ldloc.2 
    L_004a: box int32
    L_004f: ldloc.0 
    L_0050: box [mscorlib]System.Nullable`1<valuetype [mscorlib]System.Decimal>
    L_0055: ldloc.1 
    L_0056: call void [mscorlib]System.Console::WriteLine(string, object, object, object)
    L_005b: nop 
    L_005c: nop 
    L_005d: ldloc.2 
    L_005e: ldc.i4.1 
    L_005f: add 
    L_0060: stloc.2 
    L_0061: ldloc.2 
    L_0062: ldc.i4.s 10
    L_0064: clt 
    L_0066: stloc.s flag
    L_0068: ldloc.s flag
    L_006a: brtrue.s L_0005
    L_006c: ret 
}

秘密を共有します。.locals init ( ... )指定されている順序を除いて、IL はまったく同じです。ループ内で変数を宣言すると、追加の IL は発生しません。

于 2008-12-05T21:45:30.383 に答える
8

とにかく、forループ内に宣言を入れてはいけません。変数を何度も作成するための追加のリソースを消費します。これは、反復ごとに変数をクリアするだけでよい場合です。

いいえ、しませ!あなたのアドバイスの正反対を行う必要があります。ただし、変数をリセットする方が効率的であったとしても、可能な限り狭い範囲で変数を宣言する方がはるかに明確です。そして、明快さはいつでも(ほぼ)マイクロ最適化に勝ちます。さらに、1つの変数、1つの使用法。変数を不必要に再利用しないでください。

とはいえ、ここでは変数はリセットも再初期化もされません。実際、変数はC#によって初期化されることさえありません。これを修正するには、それらを初期化して実行するだけです。

于 2008-12-05T21:21:55.580 に答える
2

コードの出力は次のとおりです。

Loop 0: testDecimal=0 - testString=0
Loop 1: testDecimal= - testString=
Loop 2: testDecimal=6.25 - testString=2
Loop 3: testDecimal= - testString=
Loop 4: testDecimal=12.5 - testString=4
Loop 5: testDecimal= - testString=
Loop 6: testDecimal=18.75 - testString=6
Loop 7: testDecimal= - testString=
Loop 8: testDecimal=25 - testString=8
Loop 9: testDecimal= - testString=

この出力を生成するために、投稿されたソースでは何も変更していません。例外もスローしないことに注意してください。

于 2008-12-05T21:18:01.873 に答える
0

NullReferenceExceptionエラーが発生しましたか?

上記のコードから、変数をnullとして割り当てた後に変数を出力しようとすると、ループの奇数回の反復ごとにそのエラーが発生します。

于 2008-12-05T21:10:04.073 に答える
0

ここで奇妙なことが起こっています。初期化されていない場合は、コンパイル エラーが発生するはずです。

私があなたのコードを実行したとき、奇数ループには何もなく、偶数ループには正しい数値が得られました。

于 2008-12-05T22:15:19.137 に答える
-1

これには私も驚きました。「for」ループ内でスコープが変更されると思っていたでしょう。そうではないようです。値は保持されています。コンパイラは、「for」ループが最初に入力されたときに変数を 1 回宣言するのに十分賢いようです。

とにかく「for」ループ内に宣言を入れるべきではないという以前の投稿に同意します。変数を初期化すると、ループごとにリソースを消費します。

しかし、「for」ループの内部を関数に分割すると (これはまだ悪いことだとわかっています)。スコープ外に出ると、毎回変数が作成されます。

private void LoopTest()
{
    for (int i =0; i< 10; i++)
    {
        DoWork(i);
    }
}

private void Work(int i)
{
    decimal? testDecimal;
    string testString;

    switch (i % 2)
    {
        case 0:
            testDecimal = i / (decimal).32;
            testString = i.ToString();
            break;
        default:
            testDecimal = null;
            testString = null;
            break;
    }
    Console.WriteLine( "Loop {0}: testDecimal={1} - testString={2}", i, testDecimal , testString );
}

少なくとも、私は何か新しいことを学びました。ループ内での変数の宣言が実際にいかに悪いかだけでなく。

于 2008-12-05T21:44:35.737 に答える