在FOR循环中声明变量

生产中发生了一个奇怪的错误,我被要求调查。
该问题被追溯到在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语句很大,并且在一些“case”中检查了一个类’属性,就像if(myObject.Prop!= null)然后testString = myObject.Stringval。 ..在开关结束时,(外部)正在检查testString == null但是它保留了最后一次迭代的值,因此不会为null,因为编码器假定变量在循环内声明。
对不起,如果我的问题和示例有点过时,我接到了关于日托的电话,因为我正在敲打它。 我应该提到我比较了来自循环内外两个变量的IL。 那么,普遍认为“显然变量不会在每个循环中重新初始化”吗?
更多信息,变量WHERE在每次迭代时被初始化,直到有人过度热心ReSharper指出“值从未使用过”并删除它们。


编辑:

伙计们,谢谢大家。 作为我的第一篇文章,我看到将来我应该有多清楚。 我的意外变量分配的原因可以放在一个没有经验的开发人员做ReSharper告诉他的一切,而不是在整个解决方案上运行“代码清理”之后运行任何unit testing。 在VSS中查看此模块的历史记录,我看到变量Where在循环外部声明,并在每次迭代时初始化。 有问题的人希望他的ReSharper显示“全部绿色”,因此“将他的变量移到更接近任务”然后“删除冗余任务”! 我认为他不会再这样做了……现在花个周末去参加他错过的所有unit testing!
如何将问题标记为已回答?

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

在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查询的内容都符合条件。

摘要

比较生成的IL用于在循环内声明变量和生成的IL用于声明循环外的变量,certificate两种变量声明样式之间没有性能差异。 (生成的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 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::.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 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 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 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::.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 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 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完全相同。 循环内的DECLARING变量导致NO ADDITIONAL IL。

你不应该把声明放在for循环中。 它吸收了额外的资源来反复创建变量,当你应该做的只是每次迭代清除变量。

不,它没有 ! 应该完成与你的建议完全相反的建议。 但即使重置变量更有效,在更严格的范围内声明变量也要清楚得多。 并且随着时间的推移,清晰度胜过微优化(几乎)。 此外,一个变量,一个用法。 不要不必要地重用变量。

也就是说,变量不会在此处重置或重新初始化 – 实际上,它们甚至不是由C#初始化的! 要解决这个问题,只需初始化它们即可完成。

以下是代码的输出:

 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= 

我没有在发布的源中更改任何内容来生成此输出。 请注意,它也没有抛出exception。

你得到NullReferenceException错误?

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

这里有一些奇怪的东西,如果它们从未被初始化,那么它应该抛出一个编译错误。

当我运行你的代码时,我得到的正是我所期望的,奇数循环和偶数循环上的正确数字都没有。

这确实让我感到惊讶。 我原本认为范围会在“for”循环中发生变化。 似乎并非如此。 这些值正在保留。 编译器似乎足够聪明,可以在首次输入“for”循环时声明变量一次。

我同意以前的post,你不应该把声明放在“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 ); } 

好吧,至少我学到了一些新东西。 以及在循环中声明变量的确有多糟糕。