Resharper提供的示例代码,用于解释“可能对IEnumerable进行多次枚举”的问题。

86

有时,Resharper会发出警告:

可能多次枚举IEnumerable

这个问题在Stack Overflow上有解决方案,ReSharper网站这里也有相关说明。它提供了一些示例代码,并建议您改为这样做:

IEnumerable<string> names = GetNames().ToList();

我的问题是关于这个具体的建议:这样做是否仍会导致在2个for-each循环中枚举整个集合两次?


这个回答解决了你的问题吗?如何处理可能多次枚举IEnumerable的警告 - RBT
5个回答

196

GetNames() 返回一个 IEnumerable。所以如果你将其结果存储在变量中:

IEnumerable foo = GetNames();

每次枚举 foo 时,都会再次调用 GetNames() 方法(并非字面意义上的“再次调用”,我找不到一个能很好地解释细节的链接,但请参见IEnumerable.GetEnumerator())。

Resharper会看到这一点,并建议您将GetNames() 的枚举结果存储在本地变量中,例如通过将其实例化为列表:

IEnumerable fooEnumerated = GetNames().ToList();

只要您引用 fooEnumerated, 这将确保只枚举一次 GetNames() 的结果。

这很重要,因为通常您只想枚举一次,例如当 GetNames() 执行(缓慢的)数据库调用时。

由于您在列表中 实例化了 结果,现在不再重要您两次枚举 fooEnumerated; 您将对一个内存中的列表进行两次迭代。


2
不会的。在foreach循环中,GetEnumerator()方法只会被调用一次。真正的原因是为了避免脏数据的风险。例如,在GetNames()中,有一个SQL查询,但只查询返回IEnurable的内容。当您调用.ToList()时,将所有数据存储在内存中,几乎没有脏数据的风险。但是,如果两个循环操作之间有很长时间,如果每次执行SQL到数据库,则存在很大的脏数据风险。 - Sun Robin
@SunRobin 这是一个简化表述真相的答案,正如其中所提到的那样。我还没有抽出时间来改进它。你对“脏数据”的使用可能需要进一步解释。 - CodeCaster
6
这是来自微软的网址,告诉我们foreach循环的内部实现。您可以看到GetEnumerator()只会被调用一次。另外一个需要知道的事情是,当IEnumerable与某些ORM一起使用时,它是懒加载的。如果您只获取了枚举器的处理程序而没有将所有数据加载到内存中,则可能会在具有相同枚举器的两个操作之间产生冲突,因为有人已经修改了数据库。为避免这种情况,Resharper建议您通过.ToList()方法将数据加载到 Merroy 中。 - Sun Robin
7
这个答案有些不太准确。并非每个IEnumerable都在每次枚举时被评估,只有那些使用了“延迟执行”实现的对象才会被评估(这就是你所缺少的链接,@CodeCaster)。当底层实现没有被延迟执行时,例如此问题示例中调用了ToList()将其存储在另一个IEnumerable变量中,可以安全地忽略R#警告。 - Frédéric
@Frédéric 我知道,之前我已经道歉了。将这篇文章改进是我的待办事项之一。 :) - CodeCaster
2
@CodeCaster,没问题,我是从最近的一个链接回答中来的,我在其中添加了一个简短的解释,说明了我认为缺少的内容。如果你喜欢,可以随意重用它。然后,一旦它们不再相关,我可能会删除我的评论。 - Frédéric

11

你可以在这里指出你链接的特别之处... 但是这个链接很好 --> +1 - LuckyLikey

10

GetNames() 不会被调用两次。每当你想使用 foreach 枚举集合时,都会调用 IEnumerable.GetEnumerator() 的实现。如果在 IEnumerable.GetEnumerator() 中进行了一些昂贵的计算,这可能是需要考虑的一个原因。


5

是的,你肯定会重复枚举两次。但关键是,如果GetNames()返回一个非常昂贵的惰性Linq查询,那么它将在没有调用ToList()ToArray()的情况下计算两次


1

仅因为一个方法返回IEnumerable,并不意味着会有延迟执行。

例如:

IEnumerable<string> GetNames()
{
    Console.WriteLine("Yolo");
    return new string[] { "Fred", "Wilma", "Betty", "Barney" };
}

var names = GetNames(); // Yolo prints out here! and only here!

foreach(name in names)
{
    // Some code...
}

foreach(name in names)
{
    // Some code...
}

回到问题,如果:

a. 存在延迟执行(例如LINQ -.Where(),.Select()等):那么该方法返回一个“承诺”,知道如何迭代集合。因此,当调用.ToList()时,这种迭代发生并将列表存储在内存中。

b. 不存在延迟执行(例如方法返回一个列表):那么假设GetNames返回一个列表,那么基本上就像在该列表上执行.ToList()。

var names = GetNames().ToList(); 
//          1        2 3
  1. Yolo Prints out(Yolo被打印出来)
  2. List is returned(列表被返回)
  3. ReturnedList.ToList() is called(调用ReturnedList.ToList())

PS,我在Resharper的文档上留下了以下评论:

您好,

请在文档中明确说明只有当GetNames()实现延迟执行时才会出现问题。

例如,如果GetNames()在底层使用yield或者像大多数LINQ语句一样实现了延迟执行(.Select(),.Where()等),那么这将是一个问题。

否则,如果GetNames()在底层没有返回实现延迟执行的IEnumerable,那么这里就没有性能或数据完整性问题。例如,如果GetNames返回List。


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