在循环内部或循环外部声明变量更好吗?

时间:2021-10-05 00:23:19

Is better do:

好做的事:

variable1Type foo; 
variable2Type baa; 

foreach(var val in list) 
{
    foo = new Foo( ... ); 
    foo.x = FormatValue(val); 

    baa = new Baa(); 
    baa.main = foo; 
    baa.Do();
}

Or:

或者:

foreach(var val in list) 
{
    variable1Type foo = new Foo( ... ); 
    foo.x = FormatValue(val); 

    variable2Type baa = new Baa(); 
    baa.main = foo; 
    baa.Do();
}

The question is: What is faster 1 case or 2 case? Is the difference is insignificant? Is it the same in real applications? This may be a optimization-micro, but I really want know which is better.

问题是,什么是更快的1或2种情况?差异是否无关紧要?在实际应用中是否相同?这可能是一个微优化,但我真的想知道哪个更好。

6 个解决方案

#1


48  

Performance-wise, let's try concrete examples:

在性能方面,让我们尝试具体的例子:

public void Method1()
{
  foreach(int i in Enumerable.Range(0, 10))
  {
    int x = i * i;
    StringBuilder sb = new StringBuilder();
    sb.Append(x);
    Console.WriteLine(sb);
  }
}
public void Method2()
{
  int x;
  StringBuilder sb;
  foreach(int i in Enumerable.Range(0, 10))
  {
    x = i * i;
    sb = new StringBuilder();
    sb.Append(x);
    Console.WriteLine(sb);
  }
}

I deliberately picked both a value-type and a reference-type in case that affects things. Now, the IL for them:

我故意选择了价值类型和引用类型,以防影响到事情。现在,他们的IL:

.method public hidebysig instance void Method1() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 i,
        [1] int32 x,
        [2] class [mscorlib]System.Text.StringBuilder sb,
        [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> enumerator)
    L_0000: ldc.i4.0 
    L_0001: ldc.i4.s 10
    L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32)
    L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
    L_000d: stloc.3 
    L_000e: br.s L_002f
    L_0010: ldloc.3 
    L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
    L_0016: stloc.0 
    L_0017: ldloc.0 
    L_0018: ldloc.0 
    L_0019: mul 
    L_001a: stloc.1 
    L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
    L_0020: stloc.2 
    L_0021: ldloc.2 
    L_0022: ldloc.1 
    L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32)
    L_0028: pop 
    L_0029: ldloc.2 
    L_002a: call void [mscorlib]System.Console::WriteLine(object)
    L_002f: ldloc.3 
    L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    L_0035: brtrue.s L_0010
    L_0037: leave.s L_0043
    L_0039: ldloc.3 
    L_003a: brfalse.s L_0042
    L_003c: ldloc.3 
    L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    L_0042: endfinally 
    L_0043: ret 
    .try L_000e to L_0039 finally handler L_0039 to L_0043
}

.method public hidebysig instance void Method2() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 x,
        [1] class [mscorlib]System.Text.StringBuilder sb,
        [2] int32 i,
        [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> enumerator)
    L_0000: ldc.i4.0 
    L_0001: ldc.i4.s 10
    L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32)
    L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
    L_000d: stloc.3 
    L_000e: br.s L_002f
    L_0010: ldloc.3 
    L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
    L_0016: stloc.2 
    L_0017: ldloc.2 
    L_0018: ldloc.2 
    L_0019: mul 
    L_001a: stloc.0 
    L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
    L_0020: stloc.1 
    L_0021: ldloc.1 
    L_0022: ldloc.0 
    L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32)
    L_0028: pop 
    L_0029: ldloc.1 
    L_002a: call void [mscorlib]System.Console::WriteLine(object)
    L_002f: ldloc.3 
    L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    L_0035: brtrue.s L_0010
    L_0037: leave.s L_0043
    L_0039: ldloc.3 
    L_003a: brfalse.s L_0042
    L_003c: ldloc.3 
    L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    L_0042: endfinally 
    L_0043: ret 
    .try L_000e to L_0039 finally handler L_0039 to L_0043
}

As you can see, apart from the order on the stack the compiler happened to choose - which could just as well have been a different order - it had absolutely no effect. In turn, there really isn't anything that one is giving the jitter to make much use of that the other isn't giving it.

正如您所看到的,除了编译器碰巧选择的堆栈上的顺序之外——这也可能是一个不同的顺序——它完全没有影响。反过来,也没有任何东西是一个人用来利用另一个人没有的。

Other than that, there is one sort-of difference.

除此之外,还有一点不同。

In my Method1(), x and sb are scoped to the foreach, and cannot be accessed either deliberately or accidentally outside of it.

在我的Method1()中,x和sb的作用域是foreach,在它之外不能被有意或无意地访问。

In my Method2(), x and sb are not known at compile-time to be reliably assigned a value within the foreach (the compiler doesn't know the foreach will perform at least one loop), so use of it is forbidden.

在我的Method2()中,x和sb在编译时并不知道要在foreach中可靠地分配值(编译器不知道foreach将执行至少一个循环),因此禁止使用foreach。

So far, no real difference.

到目前为止,没有真正的区别。

I can however assign and use x and/or sb outside of the foreach. As a rule I would say that this is probably poor scoping most of the time, so I'd favour Method1, but I might have some sensible reason to want to refer to them (more realistically if they weren't possibly unassigned), in which case I'd go for Method2.

但是,我可以在foreach之外分配和使用x和/或sb。作为一种规则,我认为这在大多数时候可能是糟糕的范围界定,所以我倾向于使用Method1,但是我可能有一些合理的理由想要引用它们(更现实地说,如果它们不可能是未分配的),在这种情况下我会选择Method2。

Still, that's a matter of how the each code can be extended or not, not a difference of the code as written. Really, there's no difference.

不过,这是每个代码可以被扩展或不被扩展的问题,而不是编写的代码的差异。真的,没有差别。

#2


4  

It doesn't matter, it has no effect on performance whatsoever.

没关系,它对性能没有任何影响。

but I really want know to do right way.

但我真的想知道怎么做才是正确的。

Most will tell you inside-the-loop makes the most sense.

大多数人会告诉你,内部循环是最有意义的。

#3


3  

It's just a matter of scope. In this case, where foo and baa are only used within the for loop, it's best practice to declare them inside the loop. It's safer too.

这只是一个范围问题。在这种情况下,foo和baa只在for循环中使用,最好在循环中声明它们。它也更安全。

#4


1  

OK, I answered this without noticing where the original poster was creating a new object every time going through the loop and not just 'using' the object. So, no, there should not really be a negligible difference when it comes to performance. With this, I would go with the second method and declare the object within the loop. That way it will be cleaned up during the next GC pass and the maintains the object within the scope.

好的,我回答这个问题的时候并没有注意到最初的海报是在什么地方创建一个新对象每次循环,而不仅仅是“使用”这个对象。所以,不,在性能方面不应该有什么可以忽略的区别。有了这个,我将使用第二个方法并在循环中声明对象。这样,它将在下一次GC传递期间被清理,并在范围内维护对象。

--- I will leave up my orignal answer, just because I typed it all, and it might help someone else who searches this message later on. I promise that in the future, I will pay more attention before I try and answer the next question.

--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --我保证以后在我尝试回答下一个问题之前我会更加注意。

Tim

蒂姆

Actually, I think there is a difference. If I recall correctly, every time you create a new object = new foo() you will get that object added to the memory heap. So, by creating the objects in the loop you are going to be adding to the overhead of the system. If you know that the loop is going to be small, it's not a problem.

事实上,我认为这是有区别的。如果我没记错的话,每次创建一个新对象= new foo()时,都会将该对象添加到内存堆中。因此,通过在循环中创建对象,您将增加系统的开销。如果你知道循环是小的,这不是问题。

So, if you end up in a loop with say 1000 objects in it, you are going to be creating 1000 variables that will not be disposed of until the next garbage collection. Now hit a database where you want to do something and you have 20,000+ rows to work with... You can create quite a system demand depending on what object type you are creating.

所以,如果你在一个包含1000个对象的循环中结束,你将会创建1000个变量,这些变量直到下一个垃圾收集才会被处理。现在点击一个你想要做某事的数据库,你有2万多行要处理……根据所创建的对象类型,您可以创建相当大的系统需求。

This should be easy to test... Create an app that creates 10,000 items with a time stamp when you enter the loop and when you exit. The first time you do it, declare the variable before the loop and the next time during the loop. You might have to bump that count up much higher than 10K to see a real difference in speed though.

这应该很容易测试……创建一个应用程序,在输入循环和退出时创建10,000个带有时间戳的项。第一次执行时,在循环之前声明变量,然后在循环期间再次声明变量。你可能需要把计数提高到10K以上才能看到速度上的差异。

As well, there is the scope issue. If created in the loop, it's gone once you exit the loop, so you can't access it again. But this also helps with the clean up as the garbage collection will eventually dispose of it once it exit the loop.

此外,还有范围问题。如果在循环中创建,那么一旦退出循环,它就消失了,因此不能再次访问它。但是这也有助于清理,因为一旦垃圾收集退出循环,它最终将处理它。

Tim

蒂姆

#5


0  

Both are completely valid, not sure there's a 'right way' to do this.

两者都是完全有效的,不确定是否有“正确的方法”来实现。

Your first case is more memory-efficient (at least in the short term). Declaring your variables inside the loop will force more reallocation of memory; however, with .NETs garbage collector, as those variables go out of scope they will be cleaned up periodically, but not necessarily immediately. The difference in speed is arguably negligible.

你的第一种情况更能提高记忆效率(至少在短期内是这样)。在循环中声明变量将迫使内存重新分配;但是,有了. net垃圾收集器,当这些变量超出范围时,将定期清理它们,但不一定立即清理。速度上的差异可以说是微不足道的。

The second case is indeed a bit safer, as limiting the scope of your variables as much as possible is usually good practice.

第二种情况确实更安全一些,因为尽可能地限制变量的范围通常是良好的实践。

#6


0  

In JS the memory allocation is whole eachtime, In C#, there is no such difference usually, but if the local variable is captured by an anonymous method like lambda expression it will matter.

在JS中,内存分配每次都是完整的,在c#中,通常没有这样的差异,但是如果局部变量被像lambda表达式这样的匿名方法捕获,这就很重要了。

#1


48  

Performance-wise, let's try concrete examples:

在性能方面,让我们尝试具体的例子:

public void Method1()
{
  foreach(int i in Enumerable.Range(0, 10))
  {
    int x = i * i;
    StringBuilder sb = new StringBuilder();
    sb.Append(x);
    Console.WriteLine(sb);
  }
}
public void Method2()
{
  int x;
  StringBuilder sb;
  foreach(int i in Enumerable.Range(0, 10))
  {
    x = i * i;
    sb = new StringBuilder();
    sb.Append(x);
    Console.WriteLine(sb);
  }
}

I deliberately picked both a value-type and a reference-type in case that affects things. Now, the IL for them:

我故意选择了价值类型和引用类型,以防影响到事情。现在,他们的IL:

.method public hidebysig instance void Method1() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 i,
        [1] int32 x,
        [2] class [mscorlib]System.Text.StringBuilder sb,
        [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> enumerator)
    L_0000: ldc.i4.0 
    L_0001: ldc.i4.s 10
    L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32)
    L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
    L_000d: stloc.3 
    L_000e: br.s L_002f
    L_0010: ldloc.3 
    L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
    L_0016: stloc.0 
    L_0017: ldloc.0 
    L_0018: ldloc.0 
    L_0019: mul 
    L_001a: stloc.1 
    L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
    L_0020: stloc.2 
    L_0021: ldloc.2 
    L_0022: ldloc.1 
    L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32)
    L_0028: pop 
    L_0029: ldloc.2 
    L_002a: call void [mscorlib]System.Console::WriteLine(object)
    L_002f: ldloc.3 
    L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    L_0035: brtrue.s L_0010
    L_0037: leave.s L_0043
    L_0039: ldloc.3 
    L_003a: brfalse.s L_0042
    L_003c: ldloc.3 
    L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    L_0042: endfinally 
    L_0043: ret 
    .try L_000e to L_0039 finally handler L_0039 to L_0043
}

.method public hidebysig instance void Method2() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 x,
        [1] class [mscorlib]System.Text.StringBuilder sb,
        [2] int32 i,
        [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> enumerator)
    L_0000: ldc.i4.0 
    L_0001: ldc.i4.s 10
    L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32)
    L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
    L_000d: stloc.3 
    L_000e: br.s L_002f
    L_0010: ldloc.3 
    L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
    L_0016: stloc.2 
    L_0017: ldloc.2 
    L_0018: ldloc.2 
    L_0019: mul 
    L_001a: stloc.0 
    L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
    L_0020: stloc.1 
    L_0021: ldloc.1 
    L_0022: ldloc.0 
    L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32)
    L_0028: pop 
    L_0029: ldloc.1 
    L_002a: call void [mscorlib]System.Console::WriteLine(object)
    L_002f: ldloc.3 
    L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    L_0035: brtrue.s L_0010
    L_0037: leave.s L_0043
    L_0039: ldloc.3 
    L_003a: brfalse.s L_0042
    L_003c: ldloc.3 
    L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    L_0042: endfinally 
    L_0043: ret 
    .try L_000e to L_0039 finally handler L_0039 to L_0043
}

As you can see, apart from the order on the stack the compiler happened to choose - which could just as well have been a different order - it had absolutely no effect. In turn, there really isn't anything that one is giving the jitter to make much use of that the other isn't giving it.

正如您所看到的,除了编译器碰巧选择的堆栈上的顺序之外——这也可能是一个不同的顺序——它完全没有影响。反过来,也没有任何东西是一个人用来利用另一个人没有的。

Other than that, there is one sort-of difference.

除此之外,还有一点不同。

In my Method1(), x and sb are scoped to the foreach, and cannot be accessed either deliberately or accidentally outside of it.

在我的Method1()中,x和sb的作用域是foreach,在它之外不能被有意或无意地访问。

In my Method2(), x and sb are not known at compile-time to be reliably assigned a value within the foreach (the compiler doesn't know the foreach will perform at least one loop), so use of it is forbidden.

在我的Method2()中,x和sb在编译时并不知道要在foreach中可靠地分配值(编译器不知道foreach将执行至少一个循环),因此禁止使用foreach。

So far, no real difference.

到目前为止,没有真正的区别。

I can however assign and use x and/or sb outside of the foreach. As a rule I would say that this is probably poor scoping most of the time, so I'd favour Method1, but I might have some sensible reason to want to refer to them (more realistically if they weren't possibly unassigned), in which case I'd go for Method2.

但是,我可以在foreach之外分配和使用x和/或sb。作为一种规则,我认为这在大多数时候可能是糟糕的范围界定,所以我倾向于使用Method1,但是我可能有一些合理的理由想要引用它们(更现实地说,如果它们不可能是未分配的),在这种情况下我会选择Method2。

Still, that's a matter of how the each code can be extended or not, not a difference of the code as written. Really, there's no difference.

不过,这是每个代码可以被扩展或不被扩展的问题,而不是编写的代码的差异。真的,没有差别。

#2


4  

It doesn't matter, it has no effect on performance whatsoever.

没关系,它对性能没有任何影响。

but I really want know to do right way.

但我真的想知道怎么做才是正确的。

Most will tell you inside-the-loop makes the most sense.

大多数人会告诉你,内部循环是最有意义的。

#3


3  

It's just a matter of scope. In this case, where foo and baa are only used within the for loop, it's best practice to declare them inside the loop. It's safer too.

这只是一个范围问题。在这种情况下,foo和baa只在for循环中使用,最好在循环中声明它们。它也更安全。

#4


1  

OK, I answered this without noticing where the original poster was creating a new object every time going through the loop and not just 'using' the object. So, no, there should not really be a negligible difference when it comes to performance. With this, I would go with the second method and declare the object within the loop. That way it will be cleaned up during the next GC pass and the maintains the object within the scope.

好的,我回答这个问题的时候并没有注意到最初的海报是在什么地方创建一个新对象每次循环,而不仅仅是“使用”这个对象。所以,不,在性能方面不应该有什么可以忽略的区别。有了这个,我将使用第二个方法并在循环中声明对象。这样,它将在下一次GC传递期间被清理,并在范围内维护对象。

--- I will leave up my orignal answer, just because I typed it all, and it might help someone else who searches this message later on. I promise that in the future, I will pay more attention before I try and answer the next question.

--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --我保证以后在我尝试回答下一个问题之前我会更加注意。

Tim

蒂姆

Actually, I think there is a difference. If I recall correctly, every time you create a new object = new foo() you will get that object added to the memory heap. So, by creating the objects in the loop you are going to be adding to the overhead of the system. If you know that the loop is going to be small, it's not a problem.

事实上,我认为这是有区别的。如果我没记错的话,每次创建一个新对象= new foo()时,都会将该对象添加到内存堆中。因此,通过在循环中创建对象,您将增加系统的开销。如果你知道循环是小的,这不是问题。

So, if you end up in a loop with say 1000 objects in it, you are going to be creating 1000 variables that will not be disposed of until the next garbage collection. Now hit a database where you want to do something and you have 20,000+ rows to work with... You can create quite a system demand depending on what object type you are creating.

所以,如果你在一个包含1000个对象的循环中结束,你将会创建1000个变量,这些变量直到下一个垃圾收集才会被处理。现在点击一个你想要做某事的数据库,你有2万多行要处理……根据所创建的对象类型,您可以创建相当大的系统需求。

This should be easy to test... Create an app that creates 10,000 items with a time stamp when you enter the loop and when you exit. The first time you do it, declare the variable before the loop and the next time during the loop. You might have to bump that count up much higher than 10K to see a real difference in speed though.

这应该很容易测试……创建一个应用程序,在输入循环和退出时创建10,000个带有时间戳的项。第一次执行时,在循环之前声明变量,然后在循环期间再次声明变量。你可能需要把计数提高到10K以上才能看到速度上的差异。

As well, there is the scope issue. If created in the loop, it's gone once you exit the loop, so you can't access it again. But this also helps with the clean up as the garbage collection will eventually dispose of it once it exit the loop.

此外,还有范围问题。如果在循环中创建,那么一旦退出循环,它就消失了,因此不能再次访问它。但是这也有助于清理,因为一旦垃圾收集退出循环,它最终将处理它。

Tim

蒂姆

#5


0  

Both are completely valid, not sure there's a 'right way' to do this.

两者都是完全有效的,不确定是否有“正确的方法”来实现。

Your first case is more memory-efficient (at least in the short term). Declaring your variables inside the loop will force more reallocation of memory; however, with .NETs garbage collector, as those variables go out of scope they will be cleaned up periodically, but not necessarily immediately. The difference in speed is arguably negligible.

你的第一种情况更能提高记忆效率(至少在短期内是这样)。在循环中声明变量将迫使内存重新分配;但是,有了. net垃圾收集器,当这些变量超出范围时,将定期清理它们,但不一定立即清理。速度上的差异可以说是微不足道的。

The second case is indeed a bit safer, as limiting the scope of your variables as much as possible is usually good practice.

第二种情况确实更安全一些,因为尽可能地限制变量的范围通常是良好的实践。

#6


0  

In JS the memory allocation is whole eachtime, In C#, there is no such difference usually, but if the local variable is captured by an anonymous method like lambda expression it will matter.

在JS中,内存分配每次都是完整的,在c#中,通常没有这样的差异,但是如果局部变量被像lambda表达式这样的匿名方法捕获,这就很重要了。