.Except / Yield返回内存不足异常

时间:2017-09-11 12:09:26

标签: c# linq out-of-memory yield-return

我使用Linq的Except语句耗尽内存。

示例:

var numbers = Enumerable.Range(1, 10000000);
int i=0;
while (numbers.Any())
{
    numbers = numbers.Except(new List<int> {i++});
}

我对该方法进行了反编译,这样做也是如此,并且还给出了内存不足的异常。

var numbers = Enumerable.Range(1, 10000000);
int i=0;
while (numbers.Any())
{
   numbers = CustomExcept(numbers, new List<int>{i++});
}
private IEnumerable<int> CustomExcept(IEnumerable<int> numbers, IEnumerable<int> exceptionList)
{
    HashSet<int> set = new HashSet<int>();
    foreach (var i in exceptionList)
    {
        set.Add(i);
    }
    foreach (var i in numbers)
    {
        if (set.Add(i))
        {
           yield return i;
        }
    }
}

所以我的问题是:为什么会抛出一个内存不足的异常?

我希望垃圾收集器能够清理未使用的HashSet。

2 个答案:

答案 0 :(得分:5)

当你i==10 numbers.Any()时,那个&#34;数字&#34;不是从10到1000万的数字列表,而是:

Enumerable.Range(1, 10000000)
   .Except({0})
   .Except({1})
   .Except({2})
   // (etc)
   .Except({9})

所有这些&#34; Excepts&#34;拥有自己非常活跃的hashset。所以垃圾收集没什么。

您必须添加一些.ToList()才能真正执行这些Excepts并为垃圾收集器提供一些机会。

答案 1 :(得分:1)

我同意,这是一个非常令人惊讶的行为。在这种情况下,我也不会期望Except内存不足。

但如果你看reference source for the Enumerable class,你会看到原因。除了(通过ExceptIterator辅助方法):

  • 创建新的Set<T>
  • 迭代第二个列表并将其元素添加到集合
  • 迭代第一个列表,并为每个元素:
    • 尝试将其添加到集合
    • 如果它还没有出现,则yield会将其返回

所以只是只是做一个&#34;除了#34;它还隐含地做了一个&#34; distinct&#34;。并且为了做到这一点&#34; distinct&#34;,它也将第一个列表的所有元素添加到集合中...所以使用巨大的第一个列表,是的,你会消耗大量的内存。

我希望它能做到&#34;包含&#34;在第二个循环中,而不是&#34;添加&#34;。我也没有在documentation中看到这种行为。我看到的最接近的是描述:

  

使用默认的相等比较器来比较值,生成两个序列的集合差异。

如果它将其参数视为集合,那么删除重复项是有意义的,因为它是什么集合。但这不是我从方法名称预测的东西!

无论如何,您最好的选择可能是摆脱Except,而是将Last捕获到变量中,然后执行Where(value => value != last)