首页 文章

使用yield return的IEnumerable和Recursion

提问于
浏览
279

我有一个 IEnumerable<T> 方法,我用它来在WebForms页面中找到控件 .

该方法是递归的,当 yield return 返回递归调用的值时,我遇到一些问题,返回我想要的类型 .

我的代码如下:

public static IEnumerable<Control> 
                               GetDeepControlsByType<T>(this Control control)
    {
        foreach(Control c in control.Controls)
        {
            if (c is T)
            {
                yield return c;
            }

            if(c.Controls.Count > 0)
            {
                yield return c.GetDeepControlsByType<T>();
            }
        }
    }

这当前抛出"Cannot convert expression type"错误 . 但是,如果此方法返回类型 IEnumerable<Object> ,则代码将构建,但输出中将返回错误的类型 .

有没有办法使用 yield return 同时也使用递归?

8 回答

  • 92

    在返回 IEnumerable<T> 的方法内, yield return 必须返回 T ,而不是 IEnumerable<T> .

    更换

    yield return c.GetDeepControlsByType<T>();
    

    有:

    foreach (var x in c.GetDeepControlsByType<T>())
    {
      yield return x;
    }
    
  • 19

    您需要生成递归调用产生的每个项目:

    public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
    {
        foreach(Control c in control.Controls)
        {
            if (c is T)
            {
                yield return c;
            }
    
            if(c.Controls.Count > 0)
            {
                foreach (Control control in c.GetDeepControlsByType<T>())
                {
                    yield return control;
                }
            }
        }
    }
    

    请注意,'s a cost to recursing in this way - you'll最终会创建大量迭代器,如果您有一个非常深的控制树,这可能会产生性能问题 . 如果你想避免这种情况,你基本上需要在方法中自己进行递归,以确保只创建了一个迭代器(状态机) . 有关更多详细信息和示例实现,请参阅this question - 但这显然也增加了一定的复杂性 .

  • 6

    正如Jon Skeet和Panic上校在他们的回答中指出的那样,如果树很深,在递归方法中使用_1858409可能会导致性能问题 .

    这是一个通用的非递归扩展方法,它执行一系列树的深度优先遍历:

    public static IEnumerable<TSource> RecursiveSelect<TSource>(
        this IEnumerable<TSource> source, Func<TSource, IEnumerable<TSource>> childSelector)
    {
        var stack = new Stack<IEnumerator<TSource>>();
        var enumerator = source.GetEnumerator();
    
        try
        {
            while (true)
            {
                if (enumerator.MoveNext())
                {
                    TSource element = enumerator.Current;
                    yield return element;
    
                    stack.Push(enumerator);
                    enumerator = childSelector(element).GetEnumerator();
                }
                else if (stack.Count > 0)
                {
                    enumerator.Dispose();
                    enumerator = stack.Pop();
                }
                else
                {
                    yield break;
                }
            }
        }
        finally
        {
            enumerator.Dispose();
    
            while (stack.Count > 0) // Clean up in case of an exception.
            {
                enumerator = stack.Pop();
                enumerator.Dispose();
            }
        }
    }
    

    Eric Lippert's solution不同,RecursiveSelect直接与枚举器一起工作,因此它不需要调用Reverse(它将整个序列缓存在内存中) .

    使用RecursiveSelect,可以简单地重写OP的原始方法,如下所示:

    public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
    {
        return control.Controls.RecursiveSelect(c => c.Controls).Where(c => c is T);
    }
    
  • 12

    其他人为您提供了正确的答案,但我不认为您的案件会从屈服中受益 .

    这是一个实现同样而不屈服的片段 .

    public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
    {
       return control.Controls
                     .Where(c => c is T)
                     .Concat(control.Controls
                                    .SelectMany(c =>c.GetDeepControlsByType<T>()));
    }
    
  • 459

    您需要在第二个 yield return 中返回枚举器中的项目,而不是枚举器本身 .

    public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
    {
        foreach (Control c in control.Controls)
        {
            if (c is T)
            {
                yield return c;
            }
    
            if (c.Controls.Count > 0)
            {
                foreach (Control ctrl in c.GetDeepControlsByType<T>())
                {
                    yield return ctrl;
                }
            }
        }
    }
    
  • 9

    我认为你必须返回枚举中的每个控件 .

    public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
        {
            foreach (Control c in control.Controls)
            {
                if (c is T)
                {
                    yield return c;
                }
    
                if (c.Controls.Count > 0)
                {
                    foreach (Control childControl in c.GetDeepControlsByType<T>())
                    {
                        yield return childControl;
                    }
                }
            }
        }
    
  • 0

    Seredynski's syntax是正确的,但是你应该小心避免在递归函数中使用 yield return ,因为它是内存使用的灾难 . 请参阅https://stackoverflow.com/a/3970171/284795它随着深度爆炸性地扩展(类似的功能在我的应用中使用了10%的内存) .

    一个简单的解决方案是使用一个列表并将其传递给递归https://codereview.stackexchange.com/a/5651/754

    /// <summary>
    /// Append the descendents of tree to the given list.
    /// </summary>
    private void AppendDescendents(Tree tree, List<Tree> descendents)
    {
        foreach (var child in tree.Children)
        {
            descendents.Add(child);
            AppendDescendents(child, descendents);
        }
    }
    

    或者你可以使用堆栈和while循环来消除递归调用https://codereview.stackexchange.com/a/5661/754

  • 15

    虽然有很多好的答案,但我仍然会补充说可以使用LINQ方法来完成同样的事情 .

    例如,OP的原始代码可以重写为:

    public static IEnumerable<Control> 
                               GetDeepControlsByType<T>(this Control control)
    {
       return control.Controls.OfType<T>()
              .Union(control.Controls.SelectMany(c => c.GetDeepControlsByType<T>()));        
    }
    

相关问题