Рекурсивное выравнивание списка

Я, наверное, мог бы написать это сам, но конкретный способ, которым я пытаюсь это сделать, сбивает меня с толку. Я пытаюсь написать общий метод расширения, аналогичный другим, представленным в .NET 3.5, который возьмет вложенный IEnumerable из IEnumerables (и т. д.) И сведет его в один IEnumerable. У кого-нибудь есть идеи?

В частности, у меня проблемы с синтаксисом самого метода расширения, поэтому я могу работать над алгоритмом сглаживания.

Как представлены ваши данные? Это не так просто, как с XPath.

Ray Hayes 26.09.2008 23:40

Ничто не указывает на то, что это вопрос домашнего задания ...

Outlaw Programmer 26.09.2008 23:42

Данные - это обычные объекты.

Matt H 26.09.2008 23:43

Программист-преступник: ничего явного, но когда речь идет о синтаксисе, а кода нет, шансы хорошие, это домашнее задание. (Но OP может его удалить.)

Jon Ericson 27.09.2008 00:02

Актуально (решение Эрика Липперта со стеком для больших структур данных): stackoverflow.com/a/20335369/5962841

Mafii 01.12.2017 15:54
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
38
5
23 679
13
Перейти к ответу Данный вопрос помечен как решенный

Ответы 13

static class EnumerableExtensions
{
    public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> sequence)
    {
        foreach(var child in sequence)
            foreach(var item in child)
                yield return item;
    }
}

Может вот так? Или вы имеете в виду, что он потенциально может быть бесконечно глубоким?

форум с удовольствием продолжает есть подпись, может быть, это сработает как комментарий. общедоступный статический IEnumerable <T> Flatten <T> (эта последовательность IEnumerable <IEnumerable <T>>)

Torbjörn Gyllebring 26.09.2008 23:47

Вот и все. Он задыхался от кода и предварительных тегов (ни то, ни другое не нужно).

Derek Park 26.09.2008 23:52

Обратите внимание, что из-за общей инвариантности вы не сможете передать, скажем, List <List <int>> этому методу - List <List <int>> не является List <IEnumerable <int>>. Вот почему у меня в похожем коде есть два параметра типа. Конечно, это может измениться с C# 4 :)

Jon Skeet 27.09.2008 00:06

По сути, вам нужно иметь мастер IENumerable, который находится за пределами вашей рекурсивной функции, а затем в вашей рекурсивной функции (псевдо-код)

private void flattenList(IEnumerable<T> list)
{
    foreach (T item in list)
    {
        masterList.Add(item);

        if (item.Count > 0)
        {
            this.flattenList(item);
        }
    }
}

Хотя я действительно не уверен, что вы имеете в виду под IEnumerable, вложенным в IEnumerable ... что внутри этого? Сколько уровней вложенности? Какой последний тип? очевидно, что мой код неверен, но я надеюсь, что это заставит вас задуматься.

Ответ принят как подходящий

Хм ... Я не уверен, точно, что вы хотите здесь, но вот вариант "одного уровня":

public static IEnumerable<TElement> Flatten<TElement,TSequence> (this IEnumerable<TSequence> sequences)
    where TSequence : IEnumerable<TElement> 
{
    foreach (TSequence sequence in sequences)
    {
        foreach(TElement element in sequence)
        {
            yield return element;
        }
    }
}

Если это не то, что вы хотите, не могли бы вы поставить подпись того, что вы хотите? Если вам не нужна универсальная форма, и вы просто хотите делать то, что делают конструкторы LINQ to XML, это достаточно просто, хотя рекурсивное использование блоков итераторов относительно неэффективно. Что-то типа:

static IEnumerable Flatten(params object[] objects)
{
    // Can't easily get varargs behaviour with IEnumerable
    return Flatten((IEnumerable) objects);
}

static IEnumerable Flatten(IEnumerable enumerable)
{
    foreach (object element in enumerable)
    {
        IEnumerable candidate = element as IEnumerable;
        if (candidate != null)
        {
            foreach (object nested in candidate)
            {
                yield return nested;
            }
        }
        else
        {
            yield return element;
        }
    }
}

Обратите внимание, что это будет обрабатывать строку как последовательность символов, однако, вы можете захотеть, чтобы строки особого случая были отдельными элементами, а не сглаживали их, в зависимости от вашего варианта использования.

Это поможет?

Разве не для этого предназначен [SelectMany] [1]?

enum1.SelectMany(
    a => a.SelectMany(
        b => b.SelectMany(
            c => c.Select(
                d => d.Name
            )
        )
    )
);

Это не полностью рекурсивно, оно обрабатывает только предопределенное количество уровней, в этом случае 3, истинная рекурсия может быть неограниченным количеством уровней ...

Peter 17.03.2015 13:40

Вот расширение, которое может помочь. Он пройдет по всем узлам в вашей иерархии объектов и выберет те, которые соответствуют критериям. Предполагается, что каждый объект в вашей иерархии имеет свойство коллекции содержит его дочерние объекты.

Вот расширение:

/// 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;
}

Примеры (модульные тесты):

Сначала нам нужен объект и иерархия вложенных объектов.

Простой класс узла

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);
  }
}

И способ получения трехуровневой глубокой иерархии узлов

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;
}

Первый тест: выравнивание иерархии, без фильтрации

[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());
}

Это покажет:

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

Второй тест: получите список узлов с четным 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());
}

Это покажет:

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

@Marc Спасибо за редактирование! @lukevenediger, вы можете использовать расширение Map с такими вещами, как коллекция TreeNode для Treeview, если вы сначала получите коллекцию в форме, совместимой с IEnumerable, например: IEnumerable <TreeNode> theNodes1 = treeView2.Nodes.OfType <TreeNode> (); ... или ... IEnumerable <TreeNode> theNodes2 = treeView2.Nodes.Cast <TreeNode> () .Select (node ​​=> node);

BillW 30.11.2009 15:51

Небольшое примечание, но способ, которым значения возвращаются в модульных тестах, мог бы быть более понятным. Вы можете назвать его так: nodes.Map(i => true, n => n.Children). Я бы также не советовал называть этот метод расширения «Map», «Flatten» - более четкое описание.

Amicable 07.11.2014 17:00

Метод расширения SelectMany уже делает это.

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

SelectMany не рекурсивен. Это будет только для первого уровня.

Sarin 28.09.2011 00:24

Вот измененный Ответ Джона Скита, позволяющий использовать более "одного уровня":

static IEnumerable Flatten(IEnumerable enumerable)
{
    foreach (object element in enumerable)
    {
        IEnumerable candidate = element as IEnumerable;
        if (candidate != null)
        {
            foreach (object nested in Flatten(candidate))
            {
                yield return nested;
            }
        }
        else
        {
            yield return element;
        }
    }
}

отказ от ответственности: я не знаю C#.

То же и в Python:

#!/usr/bin/env python

def flatten(iterable):
    for item in iterable:
        if hasattr(item, '__iter__'):
            for nested in flatten(item):
                yield nested
        else:
            yield item

if __name__ == '__main__':
    for item in flatten([1,[2, 3, [[4], 5]], 6, [[[7]]], [8]]):
        print(item, end = " ")

Он печатает:

1 2 3 4 5 6 7 8 

Поскольку yield недоступен в VB, а LINQ обеспечивает как отложенное выполнение, так и краткий синтаксис, вы также можете использовать.

<Extension()>
Public Function Flatten(Of T)(ByVal objects As Generic.IEnumerable(Of T), ByVal selector As Func(Of T, Generic.IEnumerable(Of T))) As Generic.IEnumerable(Of T)
   If(objects.Any()) Then
      Return objects.Union(objects.Select(selector).Where(e => e != null).SelectMany(e => e)).Flatten(selector))
   Else
      Return objects 
   End If
End Function
public static class Extensions{
  public static IEnumerable<T> Flatten<T>(this IEnumerable<T> objects, Func<T, IEnumerable<T>> selector) where T:Component{
    if (objects.Any()){
        return objects.Union(objects.Select(selector).Where(e => e != null).SelectMany(e => e).Flatten(selector));
    }
    return objects;
  }
}

отредактировал включить:

Я подумал, что поделюсь полным примером с обработкой ошибок и подходом с единой логикой.

Рекурсивное выравнивание так же просто, как:

Версия LINQ

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>();
    }
}

Версия без LINQ

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;
            }
        }
    }
}

Дизайнерские решения

Я решил:

  • запретить сглаживание нулевого IEnumerable, это можно изменить, удалив выброс исключения и:
    • добавление source = source.EmptyIfNull(); перед return в 1-й версии
    • добавление if (source != null) перед foreach во 2-й версии
  • разрешить возврат нулевой коллекции селектором - таким образом я снимаю ответственность с вызывающего, чтобы убедиться, что список дочерних элементов не пустой, это можно изменить:
    • удаление .EmptyIfNull() в первой версии - обратите внимание, что SelectMany завершится ошибкой, если селектор вернет null
    • удаление if (children == null) continue; во второй версии - обратите внимание, что foreach завершится ошибкой при нулевом параметре IEnumerable
  • разрешить фильтрацию дочерних элементов с предложением .Where на стороне вызывающего абонента или внутри селектор детей вместо передачи параметра селектор детских фильтров:
    • это не повлияет на эффективность, потому что в обеих версиях это отложенный вызов
    • это будет смешивать другую логику с методом, и я предпочитаю, чтобы логика была разделена

Образец использования

Я использую этот метод расширения в LightSwitch, чтобы получить все элементы управления на экране:

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));
    }
}

Спасибо. Мне не хватало только части !source.Any() ? source в моей собственной версии

FindOutIslamNow 03.05.2018 13:11

Мне пришлось реализовать свое с нуля, потому что все предоставленные решения сломались бы в случае возникновения цикла, то есть дочернего элемента, указывающего на своего предка. Если у вас такие же требования, как у меня, взгляните на это (также дайте мне знать, не сломается ли мое решение при каких-либо особых обстоятельствах):

Как использовать:

var flattenlist = rootItem.Flatten(obj => obj.ChildItems, obj => obj.Id)

Код:

public static class Extensions
    {
        /// <summary>
        /// This would flatten out a recursive data structure ignoring the loops. The end result would be an enumerable which enumerates all the
        /// items in the data structure regardless of the level of nesting.
        /// </summary>
        /// <typeparam name = "T">Type of the recursive data structure</typeparam>
        /// <param name = "source">Source element</param>
        /// <param name = "childrenSelector">a function that returns the children of a given data element of type T</param>
        /// <param name = "keySelector">a function that returns a key value for each element</param>
        /// <returns>a faltten list of all the items within recursive data structure of T</returns>
        public static IEnumerable<T> Flatten<T>(this IEnumerable<T> source,
            Func<T, IEnumerable<T>> childrenSelector,
            Func<T, object> keySelector) where T : class
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (childrenSelector == null)
                throw new ArgumentNullException("childrenSelector");
            if (keySelector == null)
                throw new ArgumentNullException("keySelector");
            var stack = new Stack<T>( source);
            var dictionary = new Dictionary<object, T>();
            while (stack.Any())
            {
                var currentItem = stack.Pop();
                var currentkey = keySelector(currentItem);
                if (dictionary.ContainsKey(currentkey) == false)
                {
                    dictionary.Add(currentkey, currentItem);
                    var children = childrenSelector(currentItem);
                    if (children != null)
                    {
                        foreach (var child in children)
                        {
                            stack.Push(child);
                        }
                    }
                }
                yield return currentItem;
            }
        }

        /// <summary>
        /// This would flatten out a recursive data structure ignoring the loops. The     end result would be an enumerable which enumerates all the
        /// items in the data structure regardless of the level of nesting.
        /// </summary>
        /// <typeparam name = "T">Type of the recursive data structure</typeparam>
        /// <param name = "source">Source element</param>
        /// <param name = "childrenSelector">a function that returns the children of a     given data element of type T</param>
        /// <param name = "keySelector">a function that returns a key value for each   element</param>
        /// <returns>a faltten list of all the items within recursive data structure of T</returns>
        public static IEnumerable<T> Flatten<T>(this T source, 
            Func<T, IEnumerable<T>> childrenSelector,
            Func<T, object> keySelector) where T: class
        {
            return Flatten(new [] {source}, childrenSelector, keySelector);
        }
    }

Функция:

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;
    } 
}

Использование:

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();

Хорошо, вот еще одна версия, которая объединена примерно из 3 ответов выше.

Рекурсивный. Использует доходность. Универсальный. Необязательный предикат фильтра. Дополнительная функция выбора. Примерно настолько кратко, насколько я мог это сделать.

    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;
            }
        }
    }

Использование:

// With filter predicate, with selection function
var flatList = nodes.Flatten(n => n.IsDeleted == false, n => n.Children);

class PageViewModel { 
    public IEnumerable<PageViewModel> ChildrenPages { get; set; } 
}

Func<IEnumerable<PageViewModel>, IEnumerable<PageViewModel>> concatAll = null;
concatAll = list => list.SelectMany(l => l.ChildrenPages.Any() ? 
    concatAll(l.ChildrenPages).Union(new[] { l }) : new[] { l });

var allPages = concatAll(source).ToArray();

Другие вопросы по теме