LINQ: можем ли мы создать плоский список из иерархии

OK... название правильное...

Я не хочу иерархию из плоского списка, но точный обратный

у меня есть класс папок, в котором есть список папок, удерживаемых свойством Children. Это типичная иерархическая модель.

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

предположим

   A
   - A.1
   ----- A.1.1
   ----- A.1.2
   - A.2
   ----- A.2.1
   - A.3
   B
   - B.1
   - B.2
   ----- B.2.1
   ----- B.2.2
   ----------- B.2.2.1
   ----------- B.2.2.2 

из этой иерархии плоский список, который я ожидаю, является именно тем порядком, в котором он появляется выше!

Если LINQ не может этого сделать, может ли XSLT сделать его плоским в список xml-элементов?

7 ответов


если LINQ не может этого сделать, может ли XSLT сделать его плоским в список xml-элементы?

несколько человек показали, как это сделать с LINQ.

вот короткое и простое решение XSLT который преобразует XML-представление предоставленного списка вложенных элементов в плоский упорядоченный список элементов:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="/*">
  <xsl:apply-templates select="*[1]"/>
 </xsl:template>

 <xsl:template match="*/*">
   <xsl:copy/>
   <xsl:apply-templates select="*[1]"/>
   <xsl:apply-templates select="following-sibling::*[1]"/>
 </xsl:template>
</xsl:stylesheet>

когда это преобразование применяется к XML-представлению вашего предоставленного ввод:

<t>
    <A>
        <A.1>
            <A.1.1/>
            <A.1.2/>
        </A.1>
        <A.2>
            <A.2.1/>
        </A.2>
        <A.3/>
    </A>
    <B>
        <B.1/>
        <B.2>
            <B.2.1/>
            <B.2.2>
                <B.2.2.1/>
                <B.2.2.2/>
            </B.2.2>
        </B.2>
    </B>
</t>

производится нужная, правильно упорядоченная плоская последовательность:

<A/>
<A.1/>
<A.1.1/>
<A.1.2/>
<A.2/>
<A.2.1/>
<A.3/>
<B/>
<B.1/>
<B.2/>
<B.2.1/>
<B.2.2/>
<B.2.2.1/>
<B.2.2.2/>

обновление: вот нерекурсивный и еще более простое решение XSLT (спасибо Эндрю Уэлчу за напоминание об этом):

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/">
  <xsl:for-each select="//*">
   <xsl:copy/>
  </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

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


EDIT: теперь, когда у нас есть немного больше контекста, похоже, у вас действительно есть XML для начала. Однако мы все еще не знаем, какую обработку вы выполняете над элементами. В XSLT мая быть правильным подходом, но другим было бы использовать LINQ to XML и его Descendants способ:

var doc = XDocument.Load(stream);
var descendants = doc.Descendants("Folder");
// Use descendants now

это мая в конечном итоге еще проще, чем подход XSLT. Например, если вы хотите преобразовать его в List<string>, принимая атрибут от каждого элемента:

var doc = XDocument.Load(stream);
var names = doc.Descendants("Folder")
               .Select(x => (strong) x.Attribute("name"))
               .ToList();   

один недостаток заключается в том, что это все равно загрузит весь XML-файл в память как XElement (и т. д.) объектов. Вполне возможно, что версия XSLT может обрабатывать это потоковым способом с более эффективным использованием памяти. Дмитрий, без сомнения, может дать больше информации, если это имеет отношение.


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

теперь, если вы используете LINQ to XML, это тут поддержка его очень легко - вы можете просто использовать Descendants способ:

var allFolders = root.Descendants("Folder");

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

EDIT: Хорошо, похоже, что XML - это отвлекающий маневр. Но найти всех потомков довольно легко. Вы can сделайте это с помощью блоков итератора, но это становится довольно неприятно неэффективным довольно быстро. Вот еще одна простая альтернатива:

public IList<Folder> SelfAndDescendants()
{
    List<Folder> ret = new List<Folder>();
    AddSelfAndDescendants(ret);
    return ret;
}

private void AddSelfAndDescendants(IList<Folder> list)
{
    list.Add(this);
    foreach (var child in children)
    {
        AddSelfAndDescendants(list);
    }
}

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


вы можете использовать SelectMany, чтобы сгладить наследственность

http://msdn.microsoft.com/en-us/library/bb534336.aspx


вот метод расширения linq-style, который делает то, что вы просите (без рекурсии, обрабатываемых циклов).

    public static IEnumerable<T> WalkTreeDepthFirst<T>(this IEnumerable<T> source,
       Func<T, IEnumerable<T>> childFunction)
    {
        // http://en.wikipedia.org/wiki/Depth-first_search
        HashSet<T> seenIt = new HashSet<T>();
        Stack<T> toVisit = new Stack<T>();

        foreach (T item in source.Reverse())
        {
            toVisit.Push(item);
        }

        while (toVisit.Any())
        {
            T item = toVisit.Pop();
            if (!seenIt.Contains(item))
            {
                seenIt.Add(item);
                foreach (T child in childFunction(item).Reverse())
                {
                    toVisit.Push(child);
                }
                yield return item;
            }
        }
    }

Это было бы с моей первой попытки:

    public static IEnumerable<Folder> SelfPlusChildren(Folder f)
    {
        return new[] {f}.Concat(f.Children.SelectMany(SelfPlusChildren));
    }

вы можете написать простой метод расширения, чтобы сделать это:

public static IEnumerable<Folder> GetFolders(this Folder rootFolder)
{
    yield return rootFolder;

    foreach (var child in rootFolder.Children)
        foreach(var folder in GetFolders(child))
            yield return folder;
}

или короче с помощью SelectMany():

public static IEnumerable<Folder> GetFolders(this Folder rootFolder)
{
    yield return rootFolder;

    foreach (var folder in rootFolder.Children.SelectMany(GetFolders))
        yield return folder;
}

в .Net framework нет стандартной реализации, но вы можете реализовать ее самостоятельно.

вот как вы можете сделать это:

public static IEnumerable<T> FlattenTree<T>(this T root, Func<T, IEnumerable<T>> getChildren)
{
    var state = new Stack<T>();
    state.Push(root);

    while (state.Count != 0)
    {
        T top = state.Pop();
        yield return top;

        IEnumerable<T> children = getChildren(top) ?? Enumerable.Empty<T>();
        foreach (T child in children.Reverse())
        {
            state.Push(child);
        }
    }
}