如何处理“无限”的IEnumerable?

29

一个“无限”IEnumerable的一个简单示例是:

IEnumerable<int> Numbers() {
  int i=0;
  while(true) {
    yield return unchecked(i++);
  }
}

我知道,

foreach(int i in Numbers().Take(10)) {
  Console.WriteLine(i);
}

var q = Numbers();
foreach(int i in q.Take(10)) {
  Console.WriteLine(i);
}

两者都可以正常工作(并打印出数字0-9)。

但是,在复制或处理像q这样的表达式时,是否存在任何陷阱? 我能否依赖于它们总是被“惰性”地评估这一事实? 是否有产生无限循环的危险?


1
仅仅挑刺一下,当i = Int32.MaxValue并且你执行i++时,你的“无限”的例子不会抛出异常吗?或者它会循环到Int32.MinValue吗?嗯..... - Nelson Rothermel
你说得对。很可能会抛出溢出异常...我会进行编辑。 - Danvil
我只是挑剔一下,但你的意思还是表达清楚了。另外,我试过了,它确实会循环到Int32.MinValue。没有OverflowException异常,所以它实际上是一个无限循环。 - Nelson Rothermel
4
默认情况下,C#编译时不会检查溢出。安全的方法是始终编写unchecked(i++),因为您可以使用/checked编译,然后它会失败。使用unchecked()告诉编译器,即使使用/checked编译,如果发生溢出也没问题。 - dalo
啊哈,我每天都会学到新东西。我知道汇编语言中没有任何溢出检查,我以为“高级”语言C#会有它,但我猜这是可选的。我认为使用unchecked()也可以让你的意图清晰,这总是一件好事。而且我认为应该是unchecked { },而不是unchecked( )。 :) - Nelson Rothermel
显示剩余7条评论
5个回答

23
只要您仅调用懒惰的、非缓冲的方法,您就应该没问题。因此,SkipTakeSelect等都没问题。但是,MinCountOrderBy等会出现问题。
它可以工作,但您需要谨慎。或者注入一个Take(somethingFinite)作为安全措施(或其他自定义扩展方法,在太多数据之后抛出异常)。
例如:
public static IEnumerable<T> SanityCheck<T>(this IEnumerable<T> data, int max) {
    int i = 0;
    foreach(T item in data) {
        if(++i >= max) throw new InvalidOperationException();
        yield return item;
    }
}

1
+1 我也有一个竞争性的答案,但我只是要点赞给一个名为SanityCheck的IEnumerable扩展方法。这是最好的函数名称。我要去实现它了…… - Patrick Karcher
谢谢指出这个问题。如果原帖中实际调用了foreach(int i in Numbers()),那也会导致问题。 - Felype

9

是的,您可以保证上述代码将被惰性执行。虽然在您的代码中看起来像是永远循环,但实际上您的代码会产生类似于以下内容:

IEnumerable<int> Numbers()
{
    return new PrivateNumbersEnumerable();
}

private class PrivateNumbersEnumerable : IEnumerable<int>
{
    public IEnumerator<int> GetEnumerator() 
    { 
        return new PrivateNumbersEnumerator(); 
    }
}

private class PrivateNumbersEnumerator : IEnumerator<int>
{
    private int i;

    public bool MoveNext() { i++; return true; }   

    public int Current
    {
        get { return i; }
    }
}

(显然,这不是 确切的 生成结果,因为它非常具体化于您的代码,但它仍然相似,应该可以向您展示为什么它将被惰性评估。)


5
您需要避免使用任何试图读取到结尾的贪婪函数。这包括Enumerable扩展,如:CountToArray/ToList和聚合函数Avg/Min/Max等。

无限惰性列表没有问题,但您必须做出有意识的决策来处理它们。

使用Take来限制无限循环的影响,即使您不需要全部内容,也要设置一个上限。


2

是的,您的代码将始终在没有无限循环的情况下运行。但是稍后可能会有人来干扰事情。假设他们想要执行:

var q = Numbers().ToList();

那么,你就完了!许多“聚合”函数会让你崩溃,比如Max()

0
如果使用非懒惰求值,你的第一个例子一开始就不能按预期工作。

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