在 FOR 循环中声明变量

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 );
}

编辑:

抱歉,我因为照顾孩子而匆忙离开了。问题在于生产代码中的switch语句非常庞大,在某些“case”中,会检查类的属性,例如if(myObject.Prop!= null)则testString = myObject.Stringval...在switch结束时(外部),对testString == null进行了检查,但它保留了最后一次迭代的值,因此未被视为null,而是假定变量在循环内部声明。
如果我的问题和示例有点偏差,我很抱歉,因为我接到关于日托的电话时正在整理它。我应该提到我比较了循环内外两个变量的IL。那么,“显然变量不会在每个循环中重新初始化”的共同意见是什么?
更多信息:这些变量在每次迭代时都被初始化,直到有人过于热情地使用ReSharper指出“该值从未被使用”并将其删除。


编辑:

各位,非常感谢你们。作为我的第一篇帖子,我看到了未来需要更加清晰明了。我们意外的变量赋值的原因可以归结为一个经验不足的开发人员按照ReSharper的建议进行操作,并在对整个解决方案进行“代码清理”之后没有运行任何单元测试。通过查看VSS中此模块的历史记录,我发现变量在循环外声明并在每次迭代时初始化。这个人想让他的ReSharper显示“全部绿色”,所以“将他的变量靠近赋值”然后“删除了冗余赋值”!我不认为他会再这样做了...现在要花费周末运行他错过的所有单元测试!
如何将问题标记为已回答?


我运行了你的代码,它产生了我所期望的结果。也许你可以发布一下你得到的输出以及你期望得到的结果。 - Ryan Cook
你能澄清一下你看到的吗?由于你从未在表达式的右侧使用变量,所以它们是否被“重置”应该无关紧要。 - Geoff
请重新措辞问题,以便告诉我们您的期望和所见情况。据我所知,您正在问“为什么我看到XYZ?”然后您又说“我看到了KLM”。 - Lasse V. Karlsen
ReSharper背后有开发人员,他们想了解有关错误建议等方面的所有内容。您能否详细说明一下代码是什么,ReSharper提出了什么建议,以及代码之后的情况如何?谢谢! - Ilya Ryzhenkov
没问题。我会在 Resharper 网站上为您发布代码之前和之后的内容,Ilya。 - Simon Wilson
Simon,谢谢,你发了它们吗?我找不到。 - Ilya Ryzhenkov
7个回答

17

大多数情况下,在循环内部或外部声明变量无关紧要;明确赋值的规则确保这不会有影响。在调试器中,您偶尔可能会看到旧值(例如,在变量被赋值之前在断点中查看变量),但静态分析证明这不会影响执行代码。变量永远不会每次循环都被重置,因为显然没有必要。

在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();
在第二个示例中,“value”参数是每个实例都真正的值,因为它被捕获了。这意味着第一个示例可能会显示(例如)“4 4 4 4 4”,而第二个示例将显示0-5(任意顺序) - 即“1 2 5 3 4”。
所以:在原始代码中是否涉及到捕获?任何使用lambda、匿名方法或LINQ查询的内容都符合条件。

谢谢。我觉得我在最初的问题中表述不够清晰。我所寻找的是“明确赋值规则”,可以在< href="MSDN" rel = "nofollow noreferrer">http://msdn.microsoft.com/en-us/library/aa691172(VS.71).aspx">MSDN</href>上找到。 - Simon Wilson

15

摘要

将在循环内部声明变量生成的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代码。


2
授权,分析得很好但有点冗长。你可能想使用一个更简单的例子,不会产生如此多的IL代码。 - Konrad Rudolph
1
感谢Grant提供了解决所提出问题的证明。 - StingyJack
这里的IL代码对我来说看起来并不繁琐或难以理解,事实上,它的简洁和明晰让我有些惊讶。非常好的示例,而且观点也很明确。这展示了学习IL的价值,尽管我不情愿地承认这一点。(我有点讨厌IL的整个概念,因为我讨厌给事物添加层级,并且我讨厌运行时编译的想法。) - Shavais

8
您不应该在for循环内部放置声明。这会浪费额外的资源,因为它会一遍又一遍地创建变量,而您应该做的是在每次迭代中清除变量。
不,实际上并不是这样!应该采取与您相反的建议。但即使重置变量更有效率,也应该在其最紧密的范围内声明变量,这样更加清晰。此外,一个变量只有一个用途。不要不必要地重复使用变量。
话虽如此,在这里变量既没有被重置也没有被重新初始化 - 实际上,它们甚至没有被C#初始化。要解决这个问题,只需初始化它们就可以了。

在我们深入讨论之前,OP需要澄清期望的内容。 - StingyJack
Victor:OP 假设变量在每次循环中都是新初始化的。当然,在循环中通常不需要重新初始化变量,这不是我想说的。 - Konrad Rudolph
维克多,他必须将它们分配为null(或其他内容),否则它无法编译。 - Kevin
我已经添加了一个编辑,希望能够更加清晰地说明问题。基本上,我们很惊讶地发现变量保存了先前迭代的值。 - Simon Wilson
StingyJack: 不,我认为这仍然是错误的,因为它暗示了变量的错误范围和使用方式,这两者都会使程序更难调试。不要让对象的生命周期比需要的时间更长!也不要将同一变量用于多个值! - Konrad Rudolph
显示剩余3条评论

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=

我没有更改您发布的源代码以生成此输出。请注意,它也没有抛出异常。


0

你是否遇到了NullReferenceException错误?

从上面的代码中,你会在循环的每个奇数次迭代中得到该错误,因为你试图在将变量赋值为null之后打印它们。


当您引用一个空实例时,为什么会出现NullReferenceException? :) - leppie
他在哪里提到了NullReferenceException? - Konrad Rudolph
他没有提到它,但是看代码我找不到其他可能会产生错误的地方。我可以看到他正在引用一个空实例。 - Victor
它确实有样例代码,但那不是他的真实代码,所以如果他没有使用控制台而是使用变量,那么就会抛出NullRefExc异常。 - Victor
代码未打印任何输出。变量在 switch 外部与 null 进行比较,但在 IF 语句中仍保留之前迭代的值。 - Simon Wilson

0

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

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


我应该添加一个断点到开关上,我们惊讶地发现前一个循环的值在下一次迭代中被保留了。 - Simon Wilson

-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 );
}

好的,至少我学到了新东西。同时也意识到在循环内声明变量有多么糟糕。


在循环中声明变量有什么“不好”之处?变量应该在最紧密的范围内声明。当你写for (int i = 0; ...)时,这正是你所做的。你是否建议我们也应该写成int i; for (i = 0;...) - Grant Wagner
Ron,你和Stingy犯了同样的错误。你的假设是完全错误的。 - Konrad Rudolph
关于第一个评论,for(int i=0,...表示您期望的并且在for循环的生命周期/范围内声明一次并更新。在for循环内部声明变量将会被保留或使用资源。 - Ron Todosichuk
好的,在原始示例中,没有使用额外的资源,因为变量只创建了一次并被重用(不是预期的行为)。在我的示例中,我将逻辑提取到一个方法中。变量将在每次迭代时被创建,并且将使用额外的资源。 - Ron Todosichuk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接