Recursive List Flattening

后端 未结 13 1281
既然无缘
既然无缘 2020-11-27 04:25

I could probably write this myself, but the specific way I\'m trying to accomplish it is throwing me off. I\'m trying to write a generic extension method similar to the oth

相关标签:
13条回答
  • 2020-11-27 05:27

    Here's an extension that might help. It will traverse all nodes in your hierarchy of objects and pick out the ones that match a criteria. It assumes that each object in your hierarchy has a collection property that holds its child objects.

    Here's the extension:

    /// Traverses an object hierarchy and return a flattened list of elements
    /// based on a predicate.
    /// 
    /// TSource: The type of object in your collection.</typeparam>
    /// source: The collection of your topmost TSource objects.</param>
    /// selectorFunction: A predicate for choosing the objects you want.
    /// getChildrenFunction: A function that fetches the child collection from an object.
    /// returns: A flattened list of objects which meet the criteria in selectorFunction.
    public static IEnumerable<TSource> Map<TSource>(
      this IEnumerable<TSource> source,
      Func<TSource, bool> selectorFunction,
      Func<TSource, IEnumerable<TSource>> getChildrenFunction)
    {
      // Add what we have to the stack
      var flattenedList = source.Where(selectorFunction);
    
      // Go through the input enumerable looking for children,
      // and add those if we have them
      foreach (TSource element in source)
      {
        flattenedList = flattenedList.Concat(
          getChildrenFunction(element).Map(selectorFunction,
                                           getChildrenFunction)
        );
      }
      return flattenedList;
    }
    

    Examples (Unit Tests):

    First we need an object and a nested object hierarchy.

    A simple node class

    class Node
    {
      public int NodeId { get; set; }
      public int LevelId { get; set; }
      public IEnumerable<Node> Children { get; set; }
    
      public override string ToString()
      {
        return String.Format("Node {0}, Level {1}", this.NodeId, this.LevelId);
      }
    }
    

    And a method to get a 3-level deep hierarchy of nodes

    private IEnumerable<Node> GetNodes()
    {
      // Create a 3-level deep hierarchy of nodes
      Node[] nodes = new Node[]
        {
          new Node 
          { 
            NodeId = 1, 
            LevelId = 1, 
            Children = new Node[]
            {
              new Node { NodeId = 2, LevelId = 2, Children = new Node[] {} },
              new Node
              {
                NodeId = 3,
                LevelId = 2,
                Children = new Node[]
                {
                  new Node { NodeId = 4, LevelId = 3, Children = new Node[] {} },
                  new Node { NodeId = 5, LevelId = 3, Children = new Node[] {} }
                }
              }
            }
          },
          new Node { NodeId = 6, LevelId = 1, Children = new Node[] {} }
        };
      return nodes;
    }
    

    First Test: flatten the hierarchy, no filtering

    [Test]
    public void Flatten_Nested_Heirachy()
    {
      IEnumerable<Node> nodes = GetNodes();
      var flattenedNodes = nodes.Map(
        p => true, 
        (Node n) => { return n.Children; }
      );
      foreach (Node flatNode in flattenedNodes)
      {
        Console.WriteLine(flatNode.ToString());
      }
    
      // Make sure we only end up with 6 nodes
      Assert.AreEqual(6, flattenedNodes.Count());
    }
    

    This will show:

    Node 1, Level 1
    Node 6, Level 1
    Node 2, Level 2
    Node 3, Level 2
    Node 4, Level 3
    Node 5, Level 3
    

    Second Test: Get a list of nodes that have an even-numbered NodeId

    [Test]
    public void Only_Return_Nodes_With_Even_Numbered_Node_IDs()
    {
      IEnumerable<Node> nodes = GetNodes();
      var flattenedNodes = nodes.Map(
        p => (p.NodeId % 2) == 0, 
        (Node n) => { return n.Children; }
      );
      foreach (Node flatNode in flattenedNodes)
      {
        Console.WriteLine(flatNode.ToString());
      }
      // Make sure we only end up with 3 nodes
      Assert.AreEqual(3, flattenedNodes.Count());
    }
    

    This will show:

    Node 6, Level 1
    Node 2, Level 2
    Node 4, Level 3
    
    0 讨论(0)
  • 2020-11-27 05:30

    I thought I'd share a complete example with error handling and a single-logic apporoach.

    Recursive flattening is as simple as:

    LINQ version

    public static class IEnumerableExtensions
    {
        public static IEnumerable<T> SelectManyRecursive<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (selector == null) throw new ArgumentNullException("selector");
    
            return !source.Any() ? source :
                source.Concat(
                    source
                    .SelectMany(i => selector(i).EmptyIfNull())
                    .SelectManyRecursive(selector)
                );
        }
    
        public static IEnumerable<T> EmptyIfNull<T>(this IEnumerable<T> source)
        {
            return source ?? Enumerable.Empty<T>();
        }
    }
    

    Non-LINQ version

    public static class IEnumerableExtensions
    {
        public static IEnumerable<T> SelectManyRecursive<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (selector == null) throw new ArgumentNullException("selector");
    
            foreach (T item in source)
            {
                yield return item;
    
                var children = selector(item);
                if (children == null)
                    continue;
    
                foreach (T descendant in children.SelectManyRecursive(selector))
                {
                    yield return descendant;
                }
            }
        }
    }
    

    Design decisions

    I decided to:

    • disallow flattening of a null IEnumerable, this can be changed by removing exception throwing and:
      • adding source = source.EmptyIfNull(); before return in the 1st version
      • adding if (source != null) before foreach in the 2nd version
    • allow returning of a null collection by the selector - this way I'm removing responsibility from the caller to assure the children list isn't empty, this can be changed by:
      • removing .EmptyIfNull() in the first version - note that SelectMany will fail if null is returned by selector
      • removing if (children == null) continue; in the second version - note that foreach will fail on a null IEnumerable parameter
    • allow filtering children with .Where clause on the caller side or within the children selector rather than passing a children filter selector parameter:
      • it won't impact the efficiency because in both versions it is a deferred call
      • it would be mixing another logic with the method and I prefer to keep the logic separated

    Sample use

    I'm using this extension method in LightSwitch to obtain all controls on the screen:

    public static class ScreenObjectExtensions
    {
        public static IEnumerable<IContentItemProxy> FindControls(this IScreenObject screen)
        {
            var model = screen.Details.GetModel();
    
            return model.GetChildItems()
                .SelectManyRecursive(c => c.GetChildItems())
                .OfType<IContentItemDefinition>()
                .Select(c => screen.FindControl(c.Name));
        }
    }
    
    0 讨论(0)
  • 2020-11-27 05:30

    Isn't that what [SelectMany][1] is for?

    enum1.SelectMany(
        a => a.SelectMany(
            b => b.SelectMany(
                c => c.Select(
                    d => d.Name
                )
            )
        )
    );
    
    0 讨论(0)
  • 2020-11-27 05:30

    The SelectMany extension method does this already.

    Projects each element of a sequence to an IEnumerable<(Of <(T>)>) and flattens the resulting sequences into one sequence.

    0 讨论(0)
  • 2020-11-27 05:32

    Function:

    public static class MyExtentions
    {
        public static IEnumerable<T> RecursiveSelector<T>(this IEnumerable<T> nodes, Func<T, IEnumerable<T>> selector)
        {
            if(nodes.Any())
                return nodes.Concat(nodes.SelectMany(selector).RecursiveSelector(selector));
    
            return nodes;
        } 
    }
    

    Usage:

    var ar = new[]
    {
        new Node
        {
            Name = "1",
            Chilren = new[]
            {
                new Node
                {
                    Name = "11",
                    Children = new[]
                    {
                        new Node
                        {
                            Name = "111",
    
                        }
                    }
                }
            }
        }
    };
    
    var flattened = ar.RecursiveSelector(x => x.Children).ToList();
    
    0 讨论(0)
  • 2020-11-27 05:32

    Okay here's another version which is combined from about 3 answers above.

    Recursive. Uses yield. Generic. Optional filter predicate. Optional selection function. About as concise as I could make it.

        public static IEnumerable<TNode> Flatten<TNode>(
            this IEnumerable<TNode> nodes, 
            Func<TNode, bool> filterBy = null,
            Func<TNode, IEnumerable<TNode>> selectChildren = null
            )
        {
            if (nodes == null) yield break;
            if (filterBy != null) nodes = nodes.Where(filterBy);
    
            foreach (var node in nodes)
            {
                yield return node;
    
                var children = (selectChildren == null)
                    ? node as IEnumerable<TNode>
                    : selectChildren(node);
    
                if (children == null) continue;
    
                foreach (var child in children.Flatten(filterBy, selectChildren))
                {
                    yield return child;
                }
            }
        }
    

    Usage:

    // With filter predicate, with selection function
    var flatList = nodes.Flatten(n => n.IsDeleted == false, n => n.Children);
    
    0 讨论(0)
提交回复
热议问题