代码之家  ›  专栏  ›  技术社区  ›  Rex M

在LINQ中表示递归

  •  25
  • Rex M  · 技术社区  · 15 年前

    我正在将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 回复  |  直到 15 年前
        1
  •  20
  •   Frank Schwieterman    12 年前

    linq toxml可以很好地处理这个问题,有一个xelement.elements()/.nodes()操作来获取直接子代,还有一个xelement.subsidents()/subsidentnodes()操作来获取所有子代。你能举个例子吗?

    总结LINQ到XML的行为…导航函数每个都对应于xpath中的轴类型( http://www.w3schools.com/xpath/xpath_axes.asp )。如果导航功能选择元素,则使用轴名称。如果导航功能选择节点,轴名称将与附加的节点一起使用。

    例如,有函数Descendants()和DescendantSnode()对应于XPath的Descendants轴,返回Xelement或Xnode。

    例外情况是最常用的情况,即子轴。在xpath中,这是未指定轴时使用的轴。为此,linq to xml导航函数不是children()和children nodes(),而是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
  •  19
  •   Marc Gravell    15 年前

    首先要注意的是,lambda表达式实际上可以是递归的。不,老实说!这不容易,而且 当然 不容易阅读-见鬼,大多数LINQ提供者(除了LINQtoObjects,这是非常简单的)只要看它就会咳嗽…但是它 是可能的 . See here 完整的,血淋淋的细节(警告-可能是脑痛)。

    然而!!那可能没什么帮助…对于一个实际的方法,我会看一下 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
  •  6
  •   SirDemon    15 年前

    我将继续以这样一种方式实现它,以控制我也希望查询的深度。

    类似于descendants()的东西可以通过所有级别检索子代,而descendants(0)可以检索直接子代,descendants(1)可以获取子代和孙子代等等…

        4
  •  3
  •   Reed Copsey    15 年前

    我只需要实现两个函数来清楚地区分这两个选项(children与fulldecondants),或者重载getchildren(bool-returndecondants)。每个函数都可以实现IEnumerable,因此只需考虑它们将哪个函数传递到它们的LINQ语句中。

        5
  •  3
  •   Thomas Danecker    15 年前

    您可能想要为您的类型实现一个(扩展)方法,比如flattenrecusly。

    from item in list.FlattenRecusively() where ... select item
    
        6
  •  3
  •   Av Pinzur FryGuy    15 年前

    雷克斯,你的确开了一个有趣的讨论,但你似乎已经排除了所有的可能性——也就是说,你似乎拒绝了这两种可能性。 (1)让使用者编写递归逻辑,以及 (2)让您的节点类公开一个以上的关系。

    或者,也许你还没有完全排除(2)。我可以再考虑一种方法,它几乎和getDescendants方法(或属性)一样具有表现力,但可能不太“冗长”(取决于树的形状)。

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

    from item in AllItems where item.Ancestors.Contains(currentNode) select item
    
        7
  •  2
  •   andy    15 年前

    我必须同意弗兰克的观点。了解Linq to XML如何处理这些场景。

    实际上,我完全模拟了linq-to-xml实现,但对任何数据类型都进行了更改。为什么要重新发明轮子?

        8
  •  1
  •   Matthew Whited    15 年前

    你同意在你的物体上进行重物提升吗?(甚至没有那么重)

    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
  •  0
  •   leppie    15 年前

    我只需要使用一个扩展方法来遍历树。

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