Recursive List Flattening

后端 未结 13 1357
既然无缘
既然无缘 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: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 SelectManyRecursive(this IEnumerable source, Func> 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 EmptyIfNull(this IEnumerable source)
        {
            return source ?? Enumerable.Empty();
        }
    }
    

    Non-LINQ version

    public static class IEnumerableExtensions
    {
        public static IEnumerable SelectManyRecursive(this IEnumerable source, Func> 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 FindControls(this IScreenObject screen)
        {
            var model = screen.Details.GetModel();
    
            return model.GetChildItems()
                .SelectManyRecursive(c => c.GetChildItems())
                .OfType()
                .Select(c => screen.FindControl(c.Name));
        }
    }
    

提交回复
热议问题