Выражение рекурсии в 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. Вот базовый пример иерархии сайта, как файловая система с папками и страницами:

 (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 , (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 , (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 , (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 или только ближайшие? Меньше-навязчивый, самый низкий уровень трения, чтобы различать два намерения?

Моя цель №1 состоит в том, чтобы предоставить этому поставщику LINQ другим разработчикам, которые имеют среднее представление о LINQ и позволяют им писать как рекурсивные, так и запросы списка, не предоставляя им учебник по написанию рекурсивных lambda. Учитывая хорошее использование, я могу закодировать поставщика против этого.

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

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

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

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

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

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

XElement является подтипом XNode. XNode включает такие вещи, как HTML-tags, а также комментарии 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”))

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

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

 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 Children { get; private set; } public Node(string name) { Name = name; Children = new List(); } } static class NodeExtensions { public static IEnumerable 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 SelectDeep( this IEnumerable source, Func> 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 Descendents(this Node node) { if (node == null) throw new ArgumentNullException("node"); return node.Children.SelectDeep(n => n.Children); } } 

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

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

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

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

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

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

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

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

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

а также

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

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

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

Вы в порядке, делаете тяжелую работу в своем объекте? (это даже не так тяжело)

 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 { 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 _children; public List Children { get { if (_children == null) _children = new List(); return _children; } } public override string ToString() { return Name + " " + _id.ToString(); } #region IEnumerable Members public IEnumerator 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 } } 

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

О, подождите, я делаю это уже ! 🙂

  • Метод count count vs Count ()?
  • LINQ: когда использовать SingleOrDefault и FirstOrDefault () с критериями фильтрации
  • LINQ объединяет две таблицы данных
  • Сумма предметов в коллекции
  • группа linq смежными блоками
  • Как создать динамический метод расширения соединения LINQ
  • Оптимальный запрос LINQ для получения случайной подпапки - Shuffle
  • Обновление всех объектов в коллекции с помощью LINQ
  • Проверьте, является ли массив подмножеством другого
  • Вложенный «из» запроса LINQ, выраженный с помощью методов расширения
  • по возрастанию / убыванию в LINQ - можно изменить порядок через параметр?
  • Давайте будем гением компьютера.