web-dev-qa-db-ja.com

LINQで再帰を表現する

階層データソースにLINQプロバイダーを書き込んでいます。 APIの使用方法を示す例を記述し、それらのユースケースをサポートするようにコーディングすることで、APIを設計するのが最も簡単だと思います。

私が問題を抱えていることの1つは、LINQステートメントで「詳細なクエリ」または再帰を表現するための簡単/再利用可能/エレガントな方法です。言い換えれば、以下を区別するための最良の方法は何ですか?

from item in immediate-descendants-of-current-node where ... select item

対:

from item in all-descendants-of-current-node where ... select item

編集:上記の例のどちらも、必ずしも必要なクエリの構造を反映していないことに注意してください。私は興味がありますany再帰/深さを表現する良い方法

注意してくださいこのようなプロバイダーを実装する方法や、再帰を許可するような方法で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

クエリがライトトラックの下にあるすべてのアイテムを取得する、または直接のアイテムのみを取得するという意図を表現する最も直感的な方法は何ですか? 2つの意図を区別するための最も邪魔にならない、最も摩擦の少ない方法は?

私の一番の目標は、このLINQプロバイダーを、LINQを平均的に理解している他の開発者に引き渡して、再帰ラムダの作成に関するチュートリアルを提供せずに、再帰クエリとリストクエリの両方を作成できるようにすることです。見栄えの良い使用法を考えると、それに対してプロバイダーをコーディングできます。

追加の説明:(私はこれを伝えるのが本当に嫌です!)-このLINQプロバイダーは外部システムに対するものであり、単にオブジェクトグラフを歩いているだけではなく、この特定のケースでは再帰的ですexpressionは、実際には、内部であらゆる種類の真の再帰アクティビティに変換されます。 「深い」クエリと「浅い」クエリを区別する方法が必要です。

それで、あなたはそれを表現するための最良の方法は何だと思いますか?それとも、私が見逃していたそれを表現する標準的な方法はありますか?

25
Rex M

Linq-toXmlはこれをうまく処理し、直接の子を取得するXElement.Elements()/。Nodes()操作と、すべての子孫を取得するXElement.Descendents()/ DescendentNodes()操作があります。例としてそれを考えますか?

Linq-to-Xmlの動作を要約すると...ナビゲーション関数はそれぞれXPathの軸タイプに対応します( http://www.w3schools.com/xpath/xpath_axes.asp )。ナビゲーション機能で[要素]を選択すると、軸名が使用されます。ナビゲーション機能がノードを選択する場合、軸名はNodeを追加して使用されます。

たとえば、XPathの子孫軸に対応する関数Descendants()とDescendantsNode()があり、XElementまたはXNodeのいずれかを返します。

例外的なケースは、驚くことではありませんが、最もよく使用されるケースである子軸です。 XPathでは、これは軸が指定されていない場合に使用される軸です。このため、linq-to-xmlナビゲーション関数はChildren()とChildrenNodes()ではなく、Elements()とNodes()です。

XElementはXNodeのサブタイプです。 XNodeには、HTMLタグのようなものだけでなく、HTMLコメント、cdata、またはテキストも含まれます。 XElementsはXNodeの一種ですが、特にHTMLタグを参照します。したがって、XElementにはタグ名があり、ナビゲーション機能をサポートします。

現在、LinqからXMLへのナビゲーションのチェーンはXPathほど簡単ではありません。問題は、ナビゲーション関数がコレクションオブジェクトを返すのに対し、ナビゲーション関数は非コレクションに適用されることです。テーブルタグを直接の子として選択し、次に子孫のテーブルデータタグを選択するXPath式について考えてみます。これは「./children::table/descendants::td」または「./table/descendants::td」のように見えると思います

IEnumerable <> :: SelectMany()を使用すると、コレクションのナビゲーション関数を呼び出すことができます。上記と同等のものは、.Elements( "table")。SelectMany(T => T.Descendants( "td"))のようになります。

20

さて、最初に注意することは、実際には、ラムダ式は再帰的である可能性があるということです。いいえ、正直に言って!実行するのは簡単ではなく、確かには読みやすくありません-一体、ほとんどの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);
    }
}

繰り返しますが、再帰を回避するためにこれを最適化していませんが、簡単に実行できます。

19
Marc Gravell

クエリの深さも制御できるように実装します。

Descendants()のようなものは、すべてのレベルで子孫を取得し、Descendants(0)は直接の子を取得し、Descendants(1)は子と孫を取得します...

6
SirDemon

レックス、あなたは確かに興味深い議論を始めましたが、すべての可能性を排除したようです-つまり、(1)コンシューマーに再帰ロジックを書き込むことと、(2)ノードクラスに大なり記号を公開させることの両方を拒否しているようです1度より。

または、おそらくあなたは完全に除外していません(2)。 GetDescendentsメソッド(またはプロパティ)とほぼ同じくらい表現力のあるもう1つのアプローチを考えることができますが、(ツリーの形状によっては)それほど「重々しい」ものではないかもしれません...

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

そして

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

2つのオプション(ChildrenとFullDecendants)を明確に区別するために、2つの関数を実装するか、GetChildren(bool returnDecendants)をオーバーロードします。それぞれがIEnumerableを実装できるため、LINQステートメントに渡す関数の問題になります。

3
Reed Copsey

タイプにFlattenRecusivelyのような(拡張)メソッドを実装することをお勧めします。

from item in list.FlattenRecusively() where ... select item
3
Thomas Danecker

フランクに同意する必要があります。 LINQ-to-XMLがこれらのシナリオをどのように処理するかを見てください。

実際、私はLINQ-to-XML実装を完全にエミュレートしますが、任意のデータ型に合わせて変更します。なぜ車輪の再発明をするのですか?

2
andy

あなたはあなたのオブジェクトで重い物を持ち上げることで大丈夫ですか? (それほど重くはありません)

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
    }
}
1
Matthew Whited

私は単に拡張メソッドを使用してツリーをトラバースします。

ああ待って、私はやっている それはすでに ! :)

0
leppie