Выражение рекурсии в 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
Я просто использовал бы метод расширения для перемещения дерева.
О, подождите, я делаю который уже!:)