【问题标题】:How to use LINQ to select all descendants of a composite object如何使用 LINQ 选择复合对象的所有后代
【发布时间】:2011-03-10 15:54:37
【问题描述】:

如何使用 LINQ 使ComponentTraversal.GetDescendants() 变得更好?

问题

public static class ComponentTraversal
{
    public static IEnumerable<Component> GetDescendants(this Composite composite)
    {
        //How can I do this better using LINQ?
        IList<Component> descendants = new Component[]{};
        foreach(var child in composite.Children)
        {
            descendants.Add(child);
            if(child is Composite)
            {
                descendants.AddRange((child as Composite).GetDescendants());
            }
        }
        return descendants;
    }
}
public class Component
{
    public string Name { get; set; }
}
public class Composite: Component
{
    public IEnumerable<Component> Children { get; set; }
}
public class Leaf: Component
{
    public object Value { get; set; }
}

回答

我编辑了 Chris 的答案以提供我添加到我的公共库中的通用扩展方法。我可以看到这对其他人也有帮助,所以这里是:

    public static IEnumerable<T> GetDescendants<T>(this T component, Func<T,bool> isComposite, Func<T,IEnumerable<T>> getCompositeChildren)
    {
        var children = getCompositeChildren(component);
        return children
            .Where(isComposite)
            .SelectMany(x => x.GetDescendants(isComposite, getCompositeChildren))
            .Concat(children);
    }

谢谢克里斯!

还有,

请查看 LukeH 在http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx 的回答。他的回答总体上提供了解决此问题的更好方法,但我没有选择它,因为它不是我问题的直接答案。

【问题讨论】:

  • 如果它现在可以工作,你为什么要改变它?仅仅因为 Linq 看起来更漂亮?还是您希望获得性能提升?我个人不会因为这些原因而改变工作方法,因此“更好”是非常主观的。
  • @Bazzz,在每个子组合中,我都会实例化一个新数组。我希望有一种更具成本效益的方式,而且 LINQ 可能是有道理的。我也可能想针对 IQueryable 做这种事情,在这种情况下 LINQ 肯定会带来性能优势。然而,我想知道如何在 LINQ 中做到这一点的最大原因是我还没有弄清楚。

标签: c# linq .net-3.5 linq-to-objects composite


【解决方案1】:

避免 (1) 递归方法调用、(2) 嵌套迭代器和 (3) 很多一次性分配。这种方法避免了所有这些潜在的陷阱:

public static IEnumerable<Component> GetDescendants(this Composite composite)
{
    var stack = new Stack<Component>();
    do
    {
        if (composite != null)
        {
            // this will currently yield the children in reverse order
            // use "composite.Children.Reverse()" to maintain original order
            foreach (var child in composite.Children)
            {
                stack.Push(child);
            }
        }

        if (stack.Count == 0)
            break;

        Component component = stack.Pop();
        yield return component;

        composite = component as Composite;
    } while (true);
}

这是通用的等价物:

public static IEnumerable<T> GetDescendants<T>(this T component,
    Func<T, bool> hasChildren, Func<T, IEnumerable<T>> getChildren)
{
    var stack = new Stack<T>();
    do
    {
        if (hasChildren(component))
        {
            // this will currently yield the children in reverse order
            // use "composite.Children.Reverse()" to maintain original order
            // or let the "getChildren" delegate handle the ordering
            foreach (var child in getChildren(component))
            {
                stack.Push(child);
            }
        }

        if (stack.Count == 0)
            break;

        component = stack.Pop();
        yield return component;
    } while (true);
}

【讨论】:

  • +1,感谢您的回答。我更新了问题以包含有关此的注释。您可能提供了解决方案的最佳方法,但我没有选择您,因为它不是我发布的实际问题的答案。
  • @smartcaveman:很公平,尽管通常需要编写扩展方法并将其添加到您的工具库中,而不是尝试将内置 LINQ 方法弯曲到您的要求。例如,使用我的答案的通用版本,您可以执行 var descendents = rootComponent.GetDescendents(x =&gt; x is Composite, x =&gt; ((Composite)x).Children); 之类的操作。
  • 我会将您的扩展方法与 IEnumerable 实现一起使用,但它的缺点是它与 IQueryable 不兼容,(这就是弯曲内置 LINQ 的原因方法有时是有意义的——与现有的 LINQ 提供程序兼容)。
【解决方案2】:
var result = composite.Children.OfType<Composite>().SelectMany(child => child.GetDescendants()).Concat(composite.Children);
return result.ToList();

在将命令式语法转换为 LINQ 时,一次转换一步通常很容易。以下是它的工作原理:

  1. 这是对composite.Children 的循环,所以这将是我们应用LINQ 的集合。
  2. 循环中发生了两个常规操作,因此我们一次执行其中一个
  3. “if”语句正在执行过滤器。通常,我们会使用“Where”来执行过滤器,但在这种情况下,过滤器是基于类型的。 LINQ 为此内置了“OfType”。
  4. 对于每个子组合,我们希望递归调用 GetDescendants 并将结果添加到单个列表中。每当我们想将一个元素转换为其他东西时,我们使用 Select 或 SelectMany。由于我们希望将每个元素转换为一个列表并将它们全部合并在一起,因此我们使用 SelectMany。
  5. 最后,为了添加composite.Children 本身,我们将这些结果连接到最后。

【讨论】:

  • 请注意,嵌套迭代器可能会带来巨大的性能成本。有关详细信息,请参阅 blogs.msdn.com/b/wesdyer/archive/2007/03/23/… 的“迭代器成本”部分。
  • @LukeH,这段代码至少不会受到链接问题的影响,因为它确实嵌套得特别深,并且在每一步都将结果展平为一个列表。
  • 我在问题中放置了扩展方法的通用版本。我只是稍微修改了你的代码。如果您将其包含在答案的末尾,这可能对未来的用户有所帮助(因为他们不希望在问题中看到它)。非常感谢您的帮助。
【解决方案3】:

我不知道更好,但我认为这执行相同的逻辑:

public static IEnumerable<Component> GetDescendants(this Composite composite)
{
    return composite.Children
                .Concat(composite.Children
                            .Where(x => x is Composite)
                            .SelectMany(x => x.GetDescendants())
                );
}

它可能会更短,但你所拥有的并没有错。正如我上面所说,这应该执行相同的事情,我怀疑该功能的性能是否有所提高。

【讨论】:

  • 有一点重要的区别:这种方法使用延迟执行,只在迭代结构时支付执行成本。如果不需要探索整个结构(例如,我们可以在找到结果时将其短路),那么这具有优势。
  • 请注意,嵌套迭代器可能会带来巨大的性能成本。有关详细信息,请参阅 blogs.msdn.com/b/wesdyer/archive/2007/03/23/… 的“迭代器成本”部分。
【解决方案4】:

这是您可能想要实现迭代器的一个很好的例子。这具有以更易读的语法进行惰性求值的优点。此外,如果您需要添加额外的自定义逻辑,那么此表单更具可扩展性

 public static IEnumerable<Component> GetDescendants(this Composite composite)
    {
        foreach(var child in composite.Children)
        {
            yield return child;
            if(!(child is Composite))
               continue;

            foreach (var subChild in ((Composite)child).GetDescendants())
               yield return subChild;
        }
    }

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-21
相关资源
最近更新 更多