.Except / Yield return 内存不足异常

.Except / Yield return out of memory exception

我使用 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。

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

但如果您查看 reference source for the Enumerable class,您就会明白原因。除了(通过 ExceptIterator 辅助方法):

  • 创建一个新的 Set<T>
  • 迭代第二个列表并将其元素添加到集合中
  • 迭代第一个列表,并为每个元素:
    • 尝试将其添加到集合中
    • 如果它还不存在,yield returns它

所以它不 只是 做一个 "except",它也隐含地做一个 "distinct"。为了做到这一点 "distinct",它也将 first 列表的所有元素添加到集合中......所以有了一个巨大的第一个列表,是的,你会消耗大量内存。

我原以为它会在第二个循环中执行 "contains",而不是 "add"。我也没有在 documentation 中看到这种行为。我看到的最接近的是描述:

Produces the set difference of two sequences by using the default equality comparer to compare values.

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

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

当你在 numbers.Any()i==10 时,"numbers" 不是从 10 到 1000 万的数字列表,而是:

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

所有这些 "Excepts" 都有自己的哈希集,这些哈希集非常活跃。所以没有什么可以垃圾收集的。

您必须添加一些 .ToList() 才能真正执行这些异常并给垃圾收集器一些机会。