【问题标题】:iterator block to LINQLINQ 的迭代器块
【发布时间】:2010-09-14 21:33:23
【问题描述】:

我很难找到用于以下迭代器块的正确 LINQ 语法:

class Program
{
    class Operation
    {
        public IEnumerable<Operation> NextOperations { get; private set; }
    }
    class Item { }

    static Item GetItem(Operation operation)
    {
        return new Item();
    }

    static IEnumerable<Item> GetItems(IEnumerable<Operation> operations)
    {
        foreach (var operation in operations)
        {
            yield return GetItem(operation);

            foreach (var item in GetItems(operation.NextOperations))  // recursive
                yield return item;
        }
    }

    static void Main(string[] args)
    {
        var operations = new List<Operation>();
        foreach (var item in GetItems(operations))
        {
        }
    }
}

也许我所拥有的和它一样好?对于这个特定的代码,在显式 foreach 中的 yield return 确实是正确的解决方案?

【问题讨论】:

  • 有更好的方式将序列压缩在一起,但我不确定它们是否是一种改进。
  • 这并不是正常意义上的压缩。它更像是一棵树的遍历。
  • @recursive:我认为你是对的,但我也认为这可以被视为作为一个奇怪的、特殊的压缩情况。不过,并不是说应该
  • 请注意,这个递归解决方案在时间上是 O(nh),其中 h 是树的高度;由于树的高度介于 lg n 和 n 之间,因此介于 O(n lg n) 和 O(n^2) 之间。如果树很深,这可能会产生非常糟糕的性能。如果您编写一个具有显式堆栈管理的非递归解决方案,那么它在时间上变为 O(n),在空间上变为 O(h)。

标签: c# linq iterator yield-return


【解决方案1】:

也许我所拥有的和它一样好?

还不错。我们可以稍微改进一下。

对于这个特定的代码,在显式 foreach 中的 yield return 确实是正确的解决方案?

这是一个合理的解决方案。它易于阅读且清楚正确。正如我之前提到的,不利的一面是,如果树非常深,性能可能会不好。

我会这样做:

static IEnumerable<T> AllNodes(this T root, Func<T, IEnumerable<T>> getChildren) 
{
    var stack = new Stack<T>();
    stack.Push(root);
    while(stack.Count > 0)
    {
        var current = stack.Pop();
        yield return current;
        foreach(var child in getChildren(current).Reverse())
            stack.Push(child);
    }
} 

static void Main()      
{      
    var operation = whatever;
    var items = from op in operation.AllNodes(x=>x.NextOperations)
                select GetItem(op);
    foreach (var item in items)      
    {      
    }      
} 

请注意,仅当您关心迭代“按顺序”进行时,才需要调用 Reverse()。例如,假设操作 Alpha 有子操作 Beta、Gamma 和 Delta,而 Delta 有子操作 Zeta 和 Omega。遍历是这样的:

push Alpha
pop Alpha
yield Alpha
push Delta
push Gamma 
push Beta
pop Beta
yield Beta
pop Gamma
yield Gamma
pop Delta
yield Delta
push Omega
push Zeta
pop Zeta
yield Zeta
pop Omega
yield Omega

现在堆栈是空的,所以我们完成了,我们以“前序遍历”顺序获取项目。如果你不关心顺序,如果你只需要确保你得到所有它们,那么不要费心反转孩子,你会按照 Alpha、Delta、Omega、Zeta 的顺序得到它们, 伽马, 贝塔。

有意义吗?

【讨论】:

  • 是的,虽然我怀疑递归版本可能比显式堆栈更快,至少对于小的扁平树而言。
【解决方案2】:

LINQ 通常不擅长使用标准查询运算符进行递归。您可以编写上述更通用的形式,但您不会找到一种简洁的标准 LINQ 方式来执行此遍历。

【讨论】:

  • 那么您如何看待 Tomas 的回答?
  • @Steven:很好,但它仍然需要递归部分。您不能只在一个语句中完成所有操作,没有额外的方法和额外的变量来表示递归使用的操作。
  • 或者,根据 Lippert 的分析,使用带有显式堆栈的额外变量来代替递归。有趣的东西。
【解决方案3】:

我认为您的实施很好。但是,如果您想使用 LINQ(并将其缩短一点 - 但不会显着缩短),那么您可以使用遍历所有操作并返回当前项目后跟所有其他递归生成项目的查询来实现 GetItems

static IEnumerable<Item> GetItems(IEnumerable<Operation> operations) 
{ 
    return from op in operations
           from itm in (new[] { GetItem(op) }).Concat
                       (GetItems(op.NextOperations));
           select itm;
} 

对于每个操作,我们生成一个序列,其中包含当前操作的项目,然后是所有递归生成的项目。通过使用嵌套的from 子句,您可以遍历此集合以获得“扁平”结构。

我认为您可以通过使用支持“将元素附加到前面”的操作的功能(不可变)列表来使它更好一点——这正是我们在嵌套的from 中所做的。使用FuncList from my functional programming book(虽然这不再是惰性序列):

static FuncList<Item> GetItems(IEnumerable<Operation> operations) 
{ 
    return (from op in operations
            from itm in FuncList.Cons(GetItem(op), GetItems(op.NextOperations));
            select itm).AsFuncList();
} 

正如 Jon 所提到的,没有使用查询编写递归方面的好方法(您可以使用 lambda 函数而不是方法来编写递归查询 - 但这也好不了多少)。

【讨论】:

  • 这里有基本情况吗?看起来它会溢出堆栈。
  • @recursive:我没有尝试过,但我认为基本情况是operations 是一个空序列 - 在这种情况下,查询将立即返回空序列(结构递归与 OP 的版本相同)。
  • @Tomas:是的,我现在看到了。
  • @Dan:这就是我们拥有 Kindle 的原因。 :-)
猜你喜欢
  • 2011-07-16
  • 1970-01-01
  • 1970-01-01
  • 2020-05-18
  • 2011-02-14
  • 1970-01-01
  • 2014-07-19
  • 2012-02-17
相关资源
最近更新 更多