【问题标题】:Yield/iterator corner cases产量/迭代器极端情况
【发布时间】:2009-05-15 20:26:37
【问题描述】:

在这篇文章 (http://blogs.msdn.com/oldnewthing/archive/2008/08/13/8854601.aspx) 上,有一个关于迭代器的流行问题和一个关于极端情况的问题:

练习:考虑以下内容 片段:foreach(int i in CountTo100Twice()) { ... }

解释第 150 次通话会发生什么 到上述循环中的 MoveNext() 。 讨论它对递归的影响 枚举器(如树遍历)。

我没有运行此代码,但我假设这是一个问题,据说有文章的答案(下面提供的所有链接),但我无法在文章共享的知识中找到答案或在这篇特定文章的 cmets 中。

有谁知道答案是什么?还有哪些其他极端案例?

1)http://blogs.msdn.com/oldnewthing/archive/2008/08/12/8849519.aspx

2)http://blogs.msdn.com/oldnewthing/archive/2008/08/13/8854601.aspx

3)http://blogs.msdn.com/oldnewthing/archive/2008/08/14/8862242.aspx

4)http://blogs.msdn.com/oldnewthing/archive/2008/08/15/8868267.aspx

谢谢

【问题讨论】:

  • 你能详细说明实际问题是什么吗?
  • 问题是:解释在上述循环中第 150 次调用 MoveNext() 时会发生什么。讨论它对递归枚举器的影响(例如树遍历)。
  • 我已经发布了一系列关于迭代器极端案例的博客文章。 (感谢您的想法!)blogs.msdn.com/ericlippert/archive/tags/Iterators/default.aspx

标签: c#


【解决方案1】:

一些事情。

(1) 乔恩当然是正确的;问题是这样的嵌套迭代器为您提供了迭代器逻辑的调用堆栈。如果您正在迭代一个深度递归定义的数据结构,这可能会破坏堆栈,并且有一些简单的方法可以将应该是线性算法的东西变成二次算法。有关详细信息,请参阅 Wes 的文章。

(2) 我们可以在没有性能问题的语言中构建一种新的迭代器逻辑。我很想实现这一点,但现在它不是一个足够高的优先级。如果您对如何操作的技术细节感兴趣,请阅读this paper

(3) 有很多极端情况;已经提到的(延迟执行边界检查和延迟执行 finally 块)是最常见的两个。不幸的是,在许多版本的 C# 中,代码生成器中存在会加剧后一个问题的错误。假设你有 try { try { ... yield ... } finally { X() } } finally { Y() } - 有一些奇怪的情况你可能会遇到我们生成的代码在 X 之前意外调用 Y() () 在清理路径上,这显然是错误的。我们已经为服务包修复了这些问题,但如果您发现其他问题,请告诉我。

(4) 还有一些现存的极其晦涩的错误涉及迭代器在执行疯狂的事情时的确切行为,例如从 finally 中退出,然后分支到外部 finally,后者执行第二个冗余的 yield 中断。同样,如果您碰巧发现行为怪异的迭代器,请随时提醒我。

【讨论】:

  • 到目前为止,当我的迭代器表现异常时,这完全是因为我的代码很奇怪;)
【解决方案2】:

我怀疑他可能指的是每次调用MoveNext() 都会调用一个状态机,而该状态机又会调用另一个状态机上的MoveNext(),这使得它的效率有点低。

这是关于 here by Wes Dyerhere by Eric Lippert 的博客。

我要说的迭代器块的主要极端情况是 nothing 在第一次调用 MoveNext() 之前执行。所以这个方法:

public IEnumerable<string> ReadLines(string file)
{
    if (file == null)
    {
        throw new ArgumentNullException("file");
    }
    using (TextReader reader = File.OpenText(file))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }        
}

在您开始迭代之前实际上不会抛出异常。相反,您需要编写如下内容:

public static IEnumerable<string> ReadLines(string file)
{
    if (file == null)
    {
        throw new ArgumentNullException("file");
    }
    return ReadLinesImpl(file);
}

public static IEnumerable<string> ReadLinesImpl(string file)
{
    using (TextReader reader = File.OpenText(file))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }        
}

同样,Eric 曾就此发表过博文:part 1part 2。我也写了一个 suggestion to make life easier 的博客,虽然我怀疑它会不会有任何结果。

【讨论】:

  • 事实上,递归迭代器就像递归函数调用:它们容易出现堆栈溢出问题。而且它们更无能,因为它们创建/释放了许多临时对象(每个迭代器都是一个)。树遍历是很容易看到此类性能问题的情况之一。使用递归迭代器的哑树遍历将为每个节点创建一个临时迭代器对象...
  • 乔恩,我以为你必须在“顶级”功能中让步?请参阅 Raymond 的帖子...“作为状态机,所有 yield return 语句都必须出现在顶层。”
  • dss539:Raymond 的帖子是准确的,但返回 IEnumerable 不会将您的函数转换为状态机。这就是 Jon 的代码起作用的原因:“ReadLine”只是一个标准函数。当你想调用一个函数来为你做收益时,限制就来了,而你在一个已经在做一些收益的函数中......它不能工作。
【解决方案3】:

当您考虑try..finally 以及派生的usinglock 语句时,会出现一个有趣的关于yield/iterators 的极端情况。考虑上面发布的迭代器块 Jon Skeet:

public IEnumerable<string> ReadLines(string file)
{
    if (file == null)
    {
        throw new ArgumentNullException("file");
    }
    using (TextReader reader = File.OpenText(file))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }        
}

如果您通过手动调用 MoveNext() 几次来在 foreach 的上下文之外使用此迭代器块,并且您从未完成迭代,那么 using 会发生什么情况?答:usingfinally 部分永远不会被调用,因此永远不会在TextReader 上调用Dispose 并且永远不会关闭打开的文件。同样,假设using 被替换为lock(something)lockfinally 部分永远不会被调用,永远不会释放对象上的锁。

教训:始终避免在迭代器块中使用try..finally 及其派生词。

【讨论】:

  • 您是否也避免在其他类型中实现 IDisposable?是的,不小心的调用者最终会泄漏资源 - 但到处都是这种情况。出于安全 的目的,我不会在迭代器块中使用try/finally,但对于简单的资源释放,我会说没关系。像线路阅读器这样的东西真的很方便。
  • 该问题询问有关迭代器/产量的极端情况。我会说,是的,资源泄漏和出现奇怪的并发问题的可能性是有趣的极端情况和可能的陷阱。我只是说要避免在迭代器中使用 try..finally,而不是永远不要使用它。我想这真的归结为这样一个事实,即在任何给定收益之后的代码可能永远不会被执行,从而导致意外行为。
  • 确实,迭代器中的锁是纯粹的邪恶。锁的持有时间应尽可能短(如果存在争用,以获得高性能),并且在获取锁时运行的代码应该为锁的作者所熟知(以避免死锁)。当锁在迭代器中时,这两种最佳实践都很容易被违反。任意时间可以过去,任意代码可以在锁中运行。
猜你喜欢
  • 2019-08-22
  • 1970-01-01
  • 2016-11-10
  • 1970-01-01
  • 2017-06-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多