简体   繁体   English

如何使用 Linq 搜索分层数据

[英]How to search Hierarchical Data with Linq

I need to search a tree for data that could be anywhere in the tree.我需要在树中搜索可能位于树中任何位置的数据。 How can this be done with linq?如何用 linq 做到这一点?

class Program
{
    static void Main(string[] args) {

        var familyRoot = new Family() {Name = "FamilyRoot"};

        var familyB = new Family() {Name = "FamilyB"};
        familyRoot.Children.Add(familyB);

        var familyC = new Family() {Name = "FamilyC"};
        familyB.Children.Add(familyC);

        var familyD = new Family() {Name = "FamilyD"};
        familyC.Children.Add(familyD);

        //There can be from 1 to n levels of families.
        //Search all children, grandchildren, great grandchildren etc, for "FamilyD" and return the object.


    }
}

public class Family {
    public string Name { get; set; }
    List<Family> _children = new List<Family>();

    public List<Family> Children {
        get { return _children; }
    }
}

That's an extension to It'sNotALie.这是It'sNotALie.的扩展It'sNotALie. s answer .答案

public static class Linq
{
    public static IEnumerable<T> Flatten<T>(this T source, Func<T, IEnumerable<T>> selector)
    {
        return selector(source).SelectMany(c => Flatten(c, selector))
                               .Concat(new[] { source });
    }
}

Sample test usage:示例测试用法:

var result = familyRoot.Flatten(x => x.Children).FirstOrDefault(x => x.Name == "FamilyD");

Returns familyD object.返回familyD对象。

You can make it work on IEnumerable<T> source too:你也可以让它在IEnumerable<T>源代码上工作:

public static IEnumerable<T> Flatten<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
{
    return source.SelectMany(x => Flatten(x, selector))
        .Concat(source);
}

Another solution without recursion...另一个没有递归的解决方案......

var result = FamilyToEnumerable(familyRoot)
                .Where(f => f.Name == "FamilyD");


IEnumerable<Family> FamilyToEnumerable(Family f)
{
    Stack<Family> stack = new Stack<Family>();
    stack.Push(f);
    while (stack.Count > 0)
    {
        var family =  stack.Pop();
        yield return family;
        foreach (var child in family.Children)
            stack.Push(child);
    }
}

Simple:简单的:

familyRoot.Flatten(f => f.Children);
//you can do whatever you want with that sequence there.
//for example you could use Where on it and find the specific families, etc.

IEnumerable<T> Flatten<T>(this T source, Func<T, IEnumerable<T>> selector)
{
    return selector(source).SelectMany(c => Flatten(selector(c), selector))
                           .Concat(new[]{source});
}

So, the simplest option is to write a function that traverses your hierarchy and produces a single sequence.因此,最简单的选择是编写一个遍历层次结构并生成单个序列的函数。 This then goes at the start of your LINQ operations, eg然后在您的 LINQ 操作开始时进行,例​​如

    IEnumerable<T> Flatten<T>(this T source)
    {
      foreach(var item in source) {
        yield item;
        foreach(var child in Flatten(item.Children)
          yield child;
      }
    }

To call simply: familyRoot.Flatten().Where(n => n.Name == "Bob");简单调用: familyRoot.Flatten().Where(n => n.Name == "Bob");

A slight alternative would give you a way to quickly ignore a whole branch:一个小小的替代方案可以让您快速忽略整个分支:

    IEnumerable<T> Flatten<T>(this T source, Func<T, bool> predicate)
    {
      foreach(var item in source) {
         if (predicate(item)) {          
            yield item;
            foreach(var child in Flatten(item.Children)
               yield child;
      }
    }

Then you could do things like: family.Flatten(n => n.Children.Count > 2).Where(...)然后你可以做这样的事情: family.Flatten(n => n.Children.Count > 2).Where(...)

I like Kenneth Bo Christensen's answer using stack, it works great, it is easy to read and it is fast (and doesn't use recursion).我喜欢 Kenneth Bo Christensen 使用堆栈的回答,它工作得很好,易于阅读并且速度很快(并且不使用递归)。 The only unpleasant thing is that it reverses the order of child items (because stack is FIFO).唯一不愉快的是它颠倒了子项的顺序(因为堆栈是FIFO)。 If sort order doesn't matter to you then it's ok.如果排序顺序对您来说无关紧要,那没关系。 If it does, sorting can be achieved easily using selector(current).如果是这样,可以使用选择器(当前)轻松实现排序。 Reverse() in the foreach loop (the rest of the code is the same as in Kenneth's original post)... foreach 循环中的Reverse() (其余代码与 Kenneth 的原始帖子中的相同)...

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

Well, I guess the way is to go with the technique of working with hierarchical structures:好吧,我想方法是使用分层结构的技术:

  1. You need an anchor to make你需要一个锚来制作
  2. You need the recursion part你需要递归部分

    // Anchor rootFamily.Children.ForEach(childFamily => { if (childFamily.Name.Contains(search)) { // Your logic here return; } SearchForChildren(childFamily); }); // Recursion public void SearchForChildren(Family childFamily) { childFamily.Children.ForEach(_childFamily => { if (_childFamily.Name.Contains(search)) { // Your logic here return; } SearchForChildren(_childFamily); }); }

I have tried two of the suggested codes and made the code a bit more clear:我已经尝试了两个建议的代码并使代码更加清晰:

    public static IEnumerable<T> Flatten1<T>(this T source, Func<T, IEnumerable<T>> selector)
    {
        return selector(source).SelectMany(c => Flatten1(c, selector)).Concat(new[] { source });
    }

    public static IEnumerable<T> Flatten2<T>(this T source, Func<T, IEnumerable<T>> selector)
    {            
        var stack = new Stack<T>();
        stack.Push(source);
        while (stack.Count > 0)
        {
            var current = stack.Pop();
            yield return current;
            foreach (var child in selector(current))
                stack.Push(child);
        }
    }

Flatten2() seems to be a little bit faster but its a close run. Flatten2() 似乎快了一点,但它的运行很接近。

Some further variants on the answers of It'sNotALie., MarcinJuraszek and DamienG. It'sNotALie.、MarcinJuraszek 和 DamienG 答案的一些进一步变体。

First, the former two give a counterintuitive ordering.首先,前两者给出了违反直觉的排序。 To get a nice tree-traversal ordering to the results, just invert the concatenation (put the "source" first).要对结果进行良好的树遍历排序,只需反转串联(将“源”放在首位)。

Second, if you are working with an expensive source like EF, and you want to limit entire branches, Damien's suggestion that you inject the predicate is a good one and can still be done with Linq.其次,如果您正在使用像 EF 这样昂贵的源,并且您想限制整个分支,Damien 建议您注入谓词是一个很好的建议,并且仍然可以使用 Linq 来完成。

Finally, for an expensive source it may also be good to pre-select the fields of interest from each node with an injected selector.最后,对于昂贵的源,使用注入的选择器从每个节点预先选择感兴趣的字段也可能是好的。

Putting all these together:将所有这些放在一起:

public static IEnumerable<R> Flatten<T,R>(this T source, Func<T, IEnumerable<T>> children
    , Func<T, R> selector
    , Func<T, bool> branchpredicate = null
) {
    if (children == null) throw new ArgumentNullException("children");
    if (selector == null) throw new ArgumentNullException("selector");
    var pred = branchpredicate ?? (src => true);
    if (children(source) == null) return new[] { selector(source) };

    return new[] { selector(source) }
        .Concat(children(source)
        .Where(pred)
        .SelectMany(c => Flatten(c, children, selector, pred)));
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM