【问题标题】:Expressing recursion in LINQ在 LINQ 中表达递归
【发布时间】:2009-04-08 23:30:59
【问题描述】:

我正在为分层数据源编写 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 提供程序是针对外部系统的,它不是简单地遍历对象图,在这种特定情况下也不是递归表达式实际上可以转化为任何类型的真正递归活动。只需要一种方法来区分“深”查询和“浅”查询。

那么,你认为最好的表达方式是什么?或者有没有我错过的标准表达方式?

【问题讨论】:

  • Rex 如果不​​了解您所期望的操作类型或您所公开的分层数据结构的更多信息,则很难回答您给出的两个示例的上下文之外的问题。你能告诉我们更多吗?
  • @Wolfbyte 添加了有关数据源的说明
  • 请问:xml“后代”方法的不情愿是什么?由于这是基于迭代器块的,因此它不像缓冲所有内容。我还添加了一个“SelectDeep”(以反映标准的“SelectMany”),您可能会觉得它很有用。也许吧。
  • @Marc 对我来说,拥有一个代表小范围项目(当前子项)的对象但在该对象上拥有一个返回更大范围(所有后代)的方法是违反直觉的)。从消费者的角度来看,我希望类集合对象上的方法作用于原始对象的范围。
  • 我见过的最好的例子是:Recursive LINQ with Y-Combinator

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


【解决方案1】:

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 cmets、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"))

【讨论】:

  • 重点是给出一个 linq 提供者如何处理递归的例子,例子是 linq2xml。
【解决方案2】:

首先要注意的是,实际上 lambda 表达式可以是递归的。不,老实说!这并不容易,当然也不容易阅读 - 哎呀,大多数 LINQ 提供程序(LINQ-to-Objects 除外,它更简单)只是看着就会咳嗽它...但它是可能的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);
    }
}

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

【讨论】:

  • "lambda 表达式可以是递归的......这并不容易,当然也不容易阅读",这就是这个问题的目的!请参阅我的倒数第二段。我知道如何进行递归,但我是一个相当资深的人,正如你所说,即使对我们来说也不愉快。所以我在写提供者方面没有问题,只是在寻找一种中级友好的方式来表达它。我将针对该提供程序进行编码。
  • 哇?真的吗?你可以写一个递归的(但纯粹的)lambda 表达式?这将需要我大量的时间和注意力...... Mads 恰好聪明得可怕......
  • 我想我们说的是同一件事。递归 lambda 确实是可能的;执行它们的方法有据可查,对某人来说可能很容易。它们对我来说可行但不容易,我认为自己是一个相当资深的人,所以我很确定它们对于中级人员来说并不容易。同样,正如我的问题所述,我正在寻找一种简单的方法来表达它。我很清楚如何执行它,在那里不需要帮助(如果需要,我会为此打开另一个问题)。
  • 好的 - 我们被转移了。这句话的目的是(希望是有趣的)旁白。正如其他人所建议的那样,我的主要观点是通过一个示例来看看 LINQ-to-XML 中使用的方法。
  • 如果没有更好的建议,我可能会走 XML 风格的路线,但我对此并不完全满意,这就是为什么我没有接受它并开设了赏金。我想得到一些其他的想法。我通过谷歌找到了一些,希望看到更多。
【解决方案3】:

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

类似 Descendants() 的东西会通过所有级别检索 Descendants,而 Descendants(0) 会检索直系子女, Descendants(1) 会获得子女和孙子女等等......

【讨论】:

    【解决方案4】:

    我将只实现两个函数来清楚地区分这两个选项(Children 与 FullDecendants),或者重载 GetChildren(bool returnDecendants)。每个都可以实现 IEnumerable,因此只需将哪个函数传递到其 LINQ 语句中即可。

    【讨论】:

      【解决方案5】:

      您可能希望为您的类型实现一个(扩展)方法,例如 FlattenRecusively。

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

      【讨论】:

        【解决方案6】:

        Rex,你当然开启了一个有趣的讨论,但你似乎排除了所有可能性——也就是说,你似乎拒绝了这两种可能性 (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
        

        【讨论】:

        • 这是我正在认真考虑的一种可能性。我喜欢拥有两个代表不同范围的独立属性的想法。我无意“消除”任何可能性,我只是想促使人们跳出通常想到的这类工作的方法来思考。
        【解决方案7】:

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

        事实上,我会完全模拟 LINQ-to-XML 实现,但要针对任何数据类型进行更改。为什么要重新发明轮子?

        【讨论】:

        • 我就是这样做的。我做了一个 flattenchildren 和一个 flattentree,分别对应了 descendents 和 descendentsandself
        【解决方案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
            }
        }
        

        【讨论】:

        • 我不明白这如何解决我的问题。我不是在问如何实现递归 lambda - 事实上,我已经建立了关于递归如何工作的大部分内部工作。我试图找到一种从前端表达它的方法,以便编写 LINQ 查询的人可以直观地区分一级查询和递归查询。
        • 您能否提供一个简单的数据层次结构以及您希望看到哪些节点被选中?它可能会帮助这里的每个人更好地了解您的目标。
        • @Matt 我已经为问题添加了示例数据结构和可能的查询。
        • 一个班级统治他们!说真的,这个类的设计对我来说看起来很可疑。
        【解决方案9】:

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

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

        【讨论】:

        • 这对您的目的来说很好,但我的 LINQ 提供程序是针对外部系统的。简单地编写一个可以遍历对象图的巧妙扩展方法与这里的问题无关。如果它有帮助,您可以将其视为 LINQ to SQL。
        猜你喜欢
        • 1970-01-01
        • 2022-01-13
        • 2014-06-22
        • 1970-01-01
        • 2018-07-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-03-19
        相关资源
        最近更新 更多