在LINQ中表达递归

时间:2009-04-08 23:30:59

标签: c# .net linq recursion hierarchical-data

我正在为一个分层数据源编写一个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

编辑:请注意,以上这些示例都不一定反映我想要的查询结构。我对任何表达递归/深度的好方法感兴趣

请注意我不是问如何实现这样的提供程序,或者如何以允许递归的方式编写我的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 

如果我写:

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

表达查询获取轻型卡车下的所有项目或仅是直接项目的最直观方式是什么?区分两种意图的最少侵入性,摩擦最小的方式?

我的第一个目标是能够将这个LINQ提供程序转交给对LINQ有一般理解的其他开发人员,并允许他们编写递归和列表查询,而不给他们编写递归lambda的教程。鉴于用法看起来不错,我可以针对该代码对代码进行编码。

补充说明:(我真的在吮吸沟通!) - 这个LINQ提供程序是一个外部系统,它不是简单地走一个对象图,也不是在这个特定的情况下做一个递归表达式实际上转化为任何类型的真正的递归活动。只需要一种方法来区分“深层”查询和“浅层”查询。

那么,您认为表达它的最佳方式是什么?或者有一种标准的表达方式,我错过了吗?

9 个答案:

答案 0 :(得分:20)

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导航功能不是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”))

答案 1 :(得分:19)

嗯,首先要注意的是,实际上,lambda表达式可以是递归的。不,老实说!这样做并不容易,当然不容易阅读 - 大多数LINQ提供程序(除了LINQ-to-Objects,它更简单)只会看到咳嗽它......但 是可能的 See here了解完整的血腥细节(警告 - 很可能是脑痛)。

然而!!这可能无济于事......对于一个实用的方法,我会看看XElement等的方式...注意你可以使用Queue<T>或{删除一些递归{1}}:

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

同样,我没有对此进行优化以避免递归,但它可以很容易地完成。

答案 2 :(得分:6)

我会以这样的方式实现它,以便控制我想要查询的深度。

Descendants()之类的东西会通过所有级别检索后代,而Descendants(0)会检索直接的子级,Descendants(1)会得到子孙后代......等等...

答案 3 :(得分:3)

我只是实现两个函数来清楚地区分两个选项(Children vs. FullDecendants),或者重载GetChildren(bool returnDecendants)。每个都可以实现IEnumerable,因此它只是将它们传递到LINQ语句中的函数。

答案 4 :(得分:3)

您可能希望为您的类型实现FlattenRecusively(扩展)方法。

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

答案 5 :(得分:3)

雷克斯,你肯定开了一个有趣的讨论,但你似乎已经消除了所有可能性 - 也就是说,你似乎拒绝了  (1)让消费者写出递归逻辑,并且  (2)让你的节点类暴露大于一度的关系。

或者,或许你还没有完全排除(2)。我可以想到一种与GetDescendents方法(或属性)几乎一样富有表现力的方法,但可能不那么“笨重”(取决于树的形状)......

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

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

答案 6 :(得分:2)

我必须同意弗兰克的观点。看看LINQ-to-XML如何处理这些场景。

实际上,我完全模仿LINQ-to-XML实现,但是对于任何数据类型都要更改它。为什么重新发明轮子?

答案 7 :(得分:1)

你是否可以在你的物体中进行繁重的举重? (它甚至不那么重)

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

答案 8 :(得分:0)

我只是使用扩展方法来遍历树。

等等,我正在做that already! :)