Выражение рекурсии в LINQ

Я пишу поставщика LINQ для иерархического источника данных. Мне проще всего разработать свой API, написав примеры, показывающие, как я хочу его использовать, а затем кодирование для поддержки этих случаев использования.

Одна вещь, с которой я столкнулся, - это простой/многоразовый/элегантный способ выражения "глубокого запроса" или рекурсии в операторе LINQ. Другими словами, какой лучший способ отличить:

from item in immediate-descendants-of-current-node where ... select item

против

from item in all-descendants-of-current-node where ... select item

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

Обратите внимание. Я не спрашиваю, как реализовать такого провайдера, или как написать IQueryable или IEnumerable таким образом, чтобы разрешить рекурсию. Я задаю вопрос с точки зрения лица, пишущего запрос LINQ и использующего моего провайдера, - что интуитивно понятно для них, чтобы выразить, хотят ли они рекурсивно или нет?

Структура данных похожа на типичную файловую систему: папка может содержать коллекцию подпапок, а папка также может содержать коллекцию элементов. Таким образом, myFolder.Folders представляет все папки, которые являются непосредственными дочерними элементами myFolder, а myFolder.Items содержит все элементы сразу в myFolder. Вот базовый пример сайта hierachy, очень похожий на файловую систему с папками и страницами:

(F)Products
    (F)Light Trucks
        (F)Z150
            (I)Pictures
            (I)Specs
            (I)Reviews
        (F)Z250
            (I)Pictures
            (I)Specs
            (I)Reviews
        (F)Z350
            (I)Pictures
            (I)Specs
            (I)Reviews
        (I)Splash Page
    (F)Heavy Trucks
    (F)Consumer Vehicles
    (I)Overview 

Если я пишу:

from item in lightTrucks.Items where item.Title == "Pictures" select item

Каков самый интуитивный способ выразить намерение, что запрос получит все предметы под Light Trucks или только ближайшие? Меньше-навязчивый способ с наименьшим трением для различения двух намерений?

Цель My # 1 - превратить этого поставщика LINQ в других разработчиков, которые имеют среднее представление о LINQ и позволяют им писать как рекурсивные, так и запросы списка, не давая им учебника по написанию рекурсивных лямбда. Учитывая использование, которое выглядит хорошо, я могу указать провайдера на это.

Дополнительные разъяснения: (я действительно сосать об этом!). Этот поставщик LINQ относится к внешней системе, это не просто ходящий объектный граф, а в этом конкретном случае рекурсивный выражение фактически переводится в любую реальную рекурсивную деятельность под капотом. Просто нужно различать "глубокий" запрос и "мелкий".

Итак, как вы думаете, лучший способ выразить это? Или есть стандартный способ выразить это, что я пропустил?

Ответ 1

Linq-toXml обрабатывает этот тон, существует операция XElement.Elements()/.Nodes() для получения непосредственных дочерних элементов и операции XElement.Descendents()/DescendentNodes() для получения всех потомков. Вы считаете это в качестве примера?

Обобщение поведения Linq-to-Xml... Функции навигации соответствуют типу оси в XPath (http://www.w3schools.com/xpath/xpath_axes.asp), Если функция навигации выбирает Элементы, используется имя оси. Если функция навигации выбирает узлы, имя оси используется с добавлением Node.

Например, существуют функции Descendants() и DescendantsNode(), соответствующие оси потомков XPath, возвращающие XElement или XNode.

Случай исключения не является неожиданностью наиболее часто используемым случаем - осью детей. В XPath это ось, если ни одна ось не указана. Для этого функции навигации linq-to-xml - это не Children() и ChildrenNodes(), а Elements() и Nodes().

XElement является подтипом XNode. XNode включает такие вещи, как HTML-теги, а также комментарии HTML, cdata или текст. XElements - это тип XNode, но конкретно относится к тэгам HTML. Поэтому XElements имеют имя тега и поддерживают навигационные функции.

Теперь его не так легко подключить навигацию в Linq-to-XML, как это XPath. Проблема в том, что функции навигации возвращают объекты коллекции, а навигационные функции применяются к не-коллекциям. Рассмотрим выражение XPath, которое выбирает тег таблицы как непосредственный дочерний, а затем тег данных таблицы потомков. Я думаю, что это будет выглядеть так:./children::table/descendants::td "или". /table/descendants::td "

Использование IEnumerable < > :: SelectMany() позволяет вызвать функции навигации в коллекции. Выражение, эквивалентное приведенному выше, выглядит примерно как .Elements( "table" ). SelectMany (T = > T.Descendants( "td" ))

Ответ 2

Ну, первое, что нужно отметить, это то, что на самом деле лямбда-выражения могут быть рекурсивными. Нет, честно! Это непросто сделать, и конечно нелегко читать - черт, большинство провайдеров LINQ (за исключением LINQ-to-Objects, что намного проще) будут иметь кашель, просто глядя на это... но это возможно. Смотрите здесь для полных подробностей (предупреждение - мозговая боль, скорее всего).

Однако!! Это, вероятно, мало поможет... для практического подхода я бы посмотрел, как это делает XElement и т.д.... обратите внимание, что вы можете удалить часть рекурсии с помощью Queue<T> или Stack<T>:

using System;
using System.Collections.Generic;

static class Program {
    static void Main() {
        Node a = new Node("a"), b = new Node("b") { Children = {a}},
            c = new Node("c") { Children = {b}};
        foreach (Node node in c.Descendents()) {
            Console.WriteLine(node.Name);
        }
    }
}

class Node { // very simplified; no sanity checking etc
    public string Name { get; private set; }
    public List<Node> Children { get; private set; }
    public Node(string name) {
        Name = name;
        Children = new List<Node>();
    }
}
static class NodeExtensions {
    public static IEnumerable<Node> Descendents(this Node node) {
        if (node == null) throw new ArgumentNullException("node");
        if(node.Children.Count > 0) {
            foreach (Node child in node.Children) {
                yield return child;
                foreach (Node desc in Descendents(child)) {
                    yield return desc;
                }
            }
        }
    }
}

Альтернативой было бы написать что-то вроде SelectDeep (для имитации SelectMany для одиночных уровней):

public static class EnumerableExtensions
{
    public static IEnumerable<T> SelectDeep<T>(
        this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
    {
        foreach (T item in source)
        {
            yield return item;
            foreach (T subItem in SelectDeep(selector(item),selector))
            {
                yield return subItem;
            }
        }
    }
}
public static class NodeExtensions
{
    public static IEnumerable<Node> Descendents(this Node node)
    {
        if (node == null) throw new ArgumentNullException("node");
        return node.Children.SelectDeep(n => n.Children);
    }
}

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

Ответ 3

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

Что-то вроде Descendants() будет извлекать потомков через все уровни, в то время как Descendants (0) будет получать немедленных детей, потомки (1) получат детей и внуков и т.д....

Ответ 4

Я бы просто реализовал две функции, чтобы четко различать два варианта ( "Дети против FullDecendants" ) или перегружать GetChildren (bool returnDecendants). Каждый может реализовать IEnumerable, так что это просто вопрос, какую функцию они передают в свою инструкцию LINQ.

Ответ 5

Возможно, вам понадобится реализовать (расширение) метод, например FlattenRecusively для вашего типа.

from item in list.FlattenRecusively() where ... select item

Ответ 6

Рекс, вы, конечно, открыли интересную дискуссию, но, похоже, вы устранили все возможности - то есть вы, кажется, отвергли оба  (1) с рекурсивной логикой записи потребителя и  (2) с вашим классом node выставляют отношения более одной степени.

Или, возможно, вы не полностью исключили (2). Я могу придумать еще один подход, который почти такой же выразительный, как метод GetDescendents (или свойство), но может и не быть настолько "тяжелым" (в зависимости от формы вашего дерева)...

from item in AllItems where item.Parent == currentNode select item

и

from item in AllItems where item.Ancestors.Contains(currentNode) select item

Ответ 7

Я бы согласился с Фрэнком. посмотрите, как LINQ-to-XML обрабатывает эти сценарии.

На самом деле, я бы полностью объединил реализацию LINQ-to-XML, но изменил ее для любого типа данных. Зачем изобретать велосипед правильно?

Ответ 8

Ты в порядке с тяжелым подъемом в своем предмете? (это даже не так тяжело)

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace LinqRecursion
{
    class Program
    {
        static void Main(string[] args)
        {
            Person mom = new Person() { Name = "Karen" };
            Person me = new Person(mom) { Name = "Matt" };
            Person youngerBrother = new Person(mom) { Name = "Robbie" };
            Person olderBrother = new Person(mom) { Name = "Kevin" };
            Person nephew1 = new Person(olderBrother) { Name = "Seth" };
            Person nephew2 = new Person(olderBrother) { Name = "Bradon" };
            Person olderSister = new Person(mom) { Name = "Michelle" };

            Console.WriteLine("\tAll");
            //        All
            //Karen 0
            //Matt 1
            //Robbie 2
            //Kevin 3
            //Seth 4
            //Bradon 5
            //Michelle 6
            foreach (var item in mom)
                Console.WriteLine(item);

            Console.WriteLine("\r\n\tOdds");
            //        Odds
            //Matt 1
            //Kevin 3
            //Bradon 5
            var odds = mom.Where(p => p.ID % 2 == 1);
            foreach (var item in odds)
                Console.WriteLine(item);

            Console.WriteLine("\r\n\tEvens");
            //        Evens
            //Karen 0
            //Robbie 2
            //Seth 4
            //Michelle 6
            var evens = mom.Where(p => p.ID % 2 == 0);
            foreach (var item in evens)
                Console.WriteLine(item);

            Console.ReadLine();

        }
    }

    public class Person : IEnumerable<Person>
    {
        private static int _idRoot;

        public Person() {
            _id = _idRoot++;
        }

        public Person(Person parent) : this()
        {
            Parent = parent;
            parent.Children.Add(this);
        }

        private int _id;
        public int ID { get { return _id; } }
        public string Name { get; set; }

        public Person Parent { get; private set; }

        private List<Person> _children;
        public List<Person> Children
        {
            get
            {
                if (_children == null)
                    _children = new List<Person>();
                return _children;
            }
        }

        public override string ToString()
        {
            return Name + " " + _id.ToString();
        }

        #region IEnumerable<Person> Members

        public IEnumerator<Person> GetEnumerator()
        {
            yield return this;
            foreach (var child in this.Children)
                foreach (var item in child)
                    yield return item;
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        #endregion
    }
}

Ответ 9

Я просто использовал бы метод расширения для перемещения дерева.

О, подождите, я делаю который уже!:)