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) 然后 testString = myObject.Stringval。 .. 在切换结束时,(外部)对 testString == null 进行了检查,但它保存了上次迭代的值,因此不像编码器假设的那样为 null,并且在循环中声明了变量。
抱歉,如果我的问题和示例有点不对劲,当我把它放在一起时,我接到了关于日托的电话。我应该提到我比较了循环内外两个变量的 IL。那么,是否普遍认为“变量显然不会在每个循环上重新初始化”?
更多信息,变量 WHERE 在每次迭代中被初始化,直到有人对 ReSharper 过度热情指出“从未使用过该值”并删除它们。


编辑:

各位,谢谢大家。作为我的第一篇文章,我看到我将来应该更加清晰。我们意外变量分配的原因可以归咎于一个没有经验的开发人员做 ReSharper 告诉他的所有事情,并且在他对整个解决方案运行“代码清理”后没有运行任何单元测试。查看 VSS 中这个模块的历史,我看到变量 Where 声明在循环之外,并且在每次迭代时初始化。有问题的人希望他的 ReSharper 显示“全绿色”,因此“将他的变量移近赋值”然后“删除多余的赋值”!我认为他不会再这样做了……现在要在周末运行他错过的所有单元测试!
如何将问题标记为已回答?

4

7 回答 7

15

大多数时候,在循环内部还是外部声明变量都没有关系。明确分配的规则确保它无关紧要。在调试器中,您可能偶尔会看到旧值(即,如果您在分配断点之前查看变量),但静态分析证明这不会影响执行代码。变量永远不会在每个循环中重置,因为显然没有必要。

在 IL 级别,**通常*变量只为方法声明一次 - 循环内的放置只是为我们程序员提供便利。

但是有一个重要的例外;任何时候捕获变量,范围规则都会变得更加复杂。例如(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();

由于第二个示例中的“值”确实是每个实例,因为它已被捕获。这意味着第一个示例可能显示(例如)“4 4 4 4 4”,而第二个示例将显示 0-5(以任何顺序) - 即“1 2 5 3 4”。

那么:原始代码中是否涉及捕获?任何带有 lambda、匿名方法或 LINQ 查询的东西都符合条件。

于 2008-12-05T23:40:19.370 回答
14

概括

将生成的用于在循环内声明变量的 IL 与生成的用于在循环外声明变量的 IL 进行比较,证明这两种类型的变量声明之间没有性能差异。(生成的 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 循环中。它会为一遍又一遍地创建变量而消耗额外的资源,而您应该做的只是在每次迭代中清除变量。

不,它没有!应该做与你的建议完全相反的事情。但是,即使重置变量更有效,在其尽可能严格的范围内声明变量也会更加清晰。并且清晰胜过(几乎)任何时候的微优化。此外,一种变量,一种用法。不要不必要地重用变量。

也就是说,变量在这里没有被重置或重新初始化——实际上,它们甚至没有被 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 错误?

从上面的代码中,当您在将变量分配为空后尝试打印变量时,您会在循环的每个奇数迭代中得到该错误。

于 2008-12-05T21:10:04.073 回答
0

这里发生了一些奇怪的事情,如果它们从未被初始化,那么它应该抛出一个编译错误。

当我运行你的代码时,我得到了我所期望的,奇数循环上没有任何东西,偶数循环上没有正确的数字。

于 2008-12-05T22:15:19.137 回答
-1

这也让我感到惊讶。我原以为范围会在“for”循环中发生变化。情况似乎并非如此。值被保留。编译器似乎足够聪明,可以在第一次进入“for”循环时声明变量。

我同意之前的帖子,你不应该把声明放在“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 回答