在foreach循环内部或外部声明变量:哪种方式更快/更好?

99

这两个选项中哪一个更快/更好?

这一个:

List<User> list = new List<User>();
User u;

foreach (string s in l)
{
    u = new User();
    u.Name = s;
    list.Add(u);
}

或者这个:

List<User> list = new List<User>();

foreach (string s in l)
{
    User u = new User();
    u.Name = s;
    list.Add(u);
}

我的新手开发技能告诉我第一个更好,但我的一位朋友告诉我我错了,但是他无法给出第二个更好的理由。

这两种方法在性能上有任何区别吗?


你最终尝试了第一种方法吗?如果用户是一个类而不是结构体,在我的理解中,它将创建一个包含最后一个用户n次的列表。 - Frederik Steinmetz
10个回答

132

就性能而言,这两个示例编译成相同的IL,因此没有区别。

第二个更好,因为如果 u 仅在循环内使用,则更清楚地表达了您的意图。


11
请注意,如果变量被lambda表达式或匿名委托所捕获,则会有区别;请参见“外部变量陷阱”(Outer Variable Trap)。 - dtb
你能解释一下为什么它们都编译成相同的中间语言(IL)吗?我非常确定C#不会像JavaScript那样将变量声明提升到函数顶部。 - styfle
4
@styfle,这是您问题的答案 - David Sherret
1
以下 Stack Overflow 链接提供了更详细的答案:1) Jon Hanna2) StriplingWarrior - user3613932

14

无论如何,最好的方法是使用一个接受 Name... 参数的构造函数,或者利用花括号表示法:

foreach (string s in l)
{
    list.Add(new User(s));
}
或者
foreach (string s in l)
{
    list.Add(new User() { Name = s });
}

或者更好的方法,使用LINQ:

var list = l.Select( s => new User { Name = s});

现在,尽管你的第一个示例在某些情况下可能会更快,但第二个示例更好,因为它更易读,并且编译器可能会丢弃变量(并完全省略它),因为它没有在 foreach 的范围之外使用。


8
今日的恋尸癖评论:“或者更好,LINQ”。当然,这只是一行代码,让我们作为开发人员感觉很好。但是四行代码的版本更易理解,因此更易维护。 - Oskar Austegard
5
几乎不可能。使用我所了解的LINQ版本,我知道我的操作是不可变的,并且适用于每个元素。 - Tordek

6

声明变量不会导致任何代码执行,因此不会影响性能。

第二种方式是你想要的,如果你采用第二种方式,出错的可能性较小,因此请使用这种方式。始终尝试在最小的范围内声明变量。

此外,更好的方法是使用Linq:

List<User> users = l.Select(name => new User{ Name = name }).ToList();

2
我喜欢这句话:“总是尝试在最小的范围内声明变量。”我认为一行代码就足以很好地回答这个问题。 - Manjoor

5
每当你对性能有疑问时,唯一需要做的就是测量-在测试中运行一个循环并计时。
回答你的问题-没有测量:-)或查看生成的ilasm-任何差异在有意义的迭代次数中都不会引起注意,并且你代码中最昂贵的操作很可能是用户分配,所以要集中精力提高代码清晰度(通常应该这样做),选择2。
哦,现在很晚了,我想我只是想说不要担心这种事情或陷入这样的细节。

谢谢你的建议,我想我也会测试一些其他我一直好奇的东西呢,哈哈:D。 - Marcus
如果您想更深入地研究影响性能的因素,可以考虑使用代码分析器。即使没有其他内容,它也会让您开始了解哪些类型的代码和操作需要最多的时间。ProfileSharp和EqatecProfilers是免费且足以让您入门的工具。 - Kevin Shea

2
我去核实了这个问题。
namespace Test
{
  class Foreach
  {
    string[] names = new[] { "ABC", "MNL", "XYZ" };

    void Method1()
    {
      List<User> list = new List<User>();
      User u;

      foreach (string s in names)
      {
        u = new User();
        u.Name = s;
        list.Add(u);
      }
    }

    void Method2()
    {

      List<User> list = new List<User>();

      foreach (string s in names)
      {
        User u = new User();
        u.Name = s;
        list.Add(u);
      }
    }
  }

  public class User { public string Name; }
}

CIL(Common Intermediate Language)泄露,只有变量才会获得不同的编号。

enter image description here

所以我准备了一些更好的东西。
namespace Test
{
  class Loop
  { 

    public TimeSpan method1 = new TimeSpan();
    public TimeSpan method2 = new TimeSpan();

    Stopwatch sw = new Stopwatch();

    public void Method1()
    {
      sw.Restart();

      C c;
      C c1;
      C c2;
      C c3;
      C c4;

      int i = 1000;
      while (i-- > 0)
      {
        c = new C();
        c1 = new C();
        c2 = new C();
        c3 = new C();
        c4 = new C();        
      }

      sw.Stop();
      method1 = method1.Add(sw.Elapsed);
    }

    public void Method2()
    {
      sw.Restart();

      int i = 1000;
      while (i-- > 0)
      {
        var c = new C();
        var c1 = new C();
        var c2 = new C();
        var c3 = new C();
        var c4 = new C();
      }

      sw.Stop();
      method2 = method2.Add(sw.Elapsed);
    }
  }

  class C { }
}

CIL 没有任何区别。

enter image description here

正文:如前所述,声明并不等同于分配内存空间,因此不会对性能造成任何影响。
标题:

测试

namespace Test
{
  class Foreach
  {
    string[] names = new[] { "ABC", "MNL", "XYZ" };

    public TimeSpan method1 = new TimeSpan();
    public TimeSpan method2 = new TimeSpan();

    Stopwatch sw = new Stopwatch();

    void Method1()
    {
      sw.Restart();

      List<User> list = new List<User>();
      User u;

      foreach (string s in names)
      {
        u = new User();
        u.Name = s;
        list.Add(u);
      }

      sw.Stop();
      method1 = method1.Add(sw.Elapsed);
    }

    void Method2()
    {
      sw.Restart();

      List<User> list = new List<User>();

      foreach (string s in names)
      {
        User u = new User();
        u.Name = s;
        list.Add(u);
      }

      sw.Stop();
      method2 = method2.Add(sw.Elapsed);
    }
  }

  public class User { public string Name; }

1
第二个更好。你的意思是每次迭代都有一个新用户。

1
从技术角度来看,第一个例子会节省几个纳秒的时间,因为堆栈帧不需要被移动以分配一个新变量。但是这是如此微小的CPU时间,你几乎感觉不到它的存在,特别是当编译器可以优化掉任何差异的时候。

我非常确定CLR在循环的每次迭代中不会分配“新变量”。 - dtb
编译器可能会优化掉这个问题,但是在循环中任何变量都需要分配堆栈空间。这取决于具体的实现方式,有些实现方式可能只是保持堆栈框架不变,而另一种实现方式(比如Mono)则可能会在每次循环时释放堆栈并重新创建它。 - Erik Funkenbusch
18
在一个方法中的所有局部变量(无论是顶级变量还是循环嵌套变量)都会被编译成IL中的方法级变量。变量的空间在方法执行前分配,而不是在C#中声明的分支被执行时分配。 - dtb
1
@dtb 你有这个声明的来源吗? - styfle

1
在这种情况下,第二个版本更好。
一般来说,如果您只需要在迭代的主体中访问值,则选择第二个版本。另一方面,如果变量将在循环主体之外保持某些最终状态,则声明并使用第一个版本。


0

在性能方面不应该有明显的差异。


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